Upload RotEx inference code, demos, documentation, and pretrained checkpoints
Browse files- .gitattributes +3 -0
- README.md +306 -3
- checkpoints/README.md +53 -0
- checkpoints/full/README.md +49 -0
- checkpoints/full/best.pt +3 -0
- checkpoints/full/last.pt +3 -0
- config.py +118 -0
- demo/RotEx_Proposed.png +3 -0
- demo/VLA_Traj.png +3 -0
- demo/illumination_variation_space.jpg +3 -0
- distill_checkpoints/README.md +35 -0
- distill_checkpoints/best_distill.pt +3 -0
- flux/__init__.py +13 -0
- flux/chronflux.py +330 -0
- flux/modules/__init__.py +0 -0
- flux/modules/adaptive_blocks.py +253 -0
- flux/modules/advanced_encoders.py +256 -0
- flux/modules/attention.py +332 -0
- flux/modules/autoencoder.py +251 -0
- flux/modules/conditioner.py +34 -0
- flux/modules/image_embedders.py +69 -0
- flux/modules/layers.py +99 -0
- flux/modules/losses.py +132 -0
- flux/modules/moe.py +149 -0
- flux/modules/normalization.py +263 -0
- flux/modules/sampling.py +379 -0
- flux/utils/__init__.py +4 -0
- flux/utils/chronflux_datasets.py +26 -0
- flux/utils/logger_utils.py +19 -0
- flux/utils/math.py +48 -0
- flux/utils/target_generation.py +63 -0
- flux/utils/util.py +76 -0
- infer_1step_style.py +210 -0
- infer_multistep.py +300 -0
- infer_multistep_vid.py +300 -0
- requirements.txt +15 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
demo/illumination_variation_space.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
demo/RotEx_Proposed.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
demo/VLA_Traj.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,3 +1,306 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# [Official] RotEx: A Rotational Expert Network for Realistic Scene Synthesis Under Illumination Variation
|
| 2 |
+
|
| 3 |
+
<p align="center">
|
| 4 |
+
π <b>Paper: coming soon</b> |
|
| 5 |
+
π» <a href="https://github.com/val-utehy/RotEx"><b>Source Code</b></a> |
|
| 6 |
+
π€ <a href="https://huggingface.co/val-utehy/RotEx"><b>Hugging Face</b></a>
|
| 7 |
+
</p>
|
| 8 |
+
|
| 9 |
+
<p align="center">
|
| 10 |
+
<img src="demo/RotEx_Proposed.png" alt="Overview of RotEx and its double-stream and single-stream transformer blocks" width="1100"/>
|
| 11 |
+
</p>
|
| 12 |
+
<p align="center">
|
| 13 |
+
<em>Architecture of RotEx: content and style streams, depth conditioning, and illumination-dependent expert routing.</em>
|
| 14 |
+
</p>
|
| 15 |
+
|
| 16 |
+
> The Hugging Face commands below describe the intended weight layout; they become usable once the checkpoints are uploaded.
|
| 17 |
+
|
| 18 |
+
## Introduction
|
| 19 |
+
|
| 20 |
+
**RotEx** is a transformer-based image-to-image framework for synthesizing scenes under varying illumination conditions. It combines content latents, depth information, style features, and a continuous time-of-day variable to generate illumination variations of an input scene.
|
| 21 |
+
|
| 22 |
+
The model operates in the latent space of a FLUX autoencoder. DINOv2 supplies style features, Depth Anything V2 supplies depth maps, and time-conditioned mixture-of-experts modules adapt the synthesis process to the requested illumination. The release includes both multi-step inference and a distilled one-step inference path.
|
| 23 |
+
|
| 24 |
+
### Key Features
|
| 25 |
+
|
| 26 |
+
- **Continuous illumination control:** Specify a time-of-day value and map it to an angle through `phi = hour * 2Ο / 24`.
|
| 27 |
+
- **Illumination-dependent experts:** Route style and image features through experts conditioned on the illumination angle.
|
| 28 |
+
- **Content and depth conditioning:** Use an encoded content image and its estimated depth map during synthesis.
|
| 29 |
+
- **Two inference modes:** Use a full checkpoint with a multi-step Heun solver, or a distilled checkpoint with a single model evaluation.
|
| 30 |
+
- **Image and video processing:** Generate image sequences across hours or vary the target hour across the frames of a video.
|
| 31 |
+
- **Reference-style conditioning:** Use a separate style image to condition illumination synthesis.
|
| 32 |
+
|
| 33 |
+
## Demo
|
| 34 |
+
|
| 35 |
+
### Illumination Variation
|
| 36 |
+
|
| 37 |
+
<p align="center">
|
| 38 |
+
<img src="demo/illumination_variation_space.jpg" alt="Scene synthesis across the illumination variation space" width="1000"/>
|
| 39 |
+
</p>
|
| 40 |
+
<p align="center">
|
| 41 |
+
<em>Illustration of the illumination variation space.</em>
|
| 42 |
+
</p>
|
| 43 |
+
|
| 44 |
+
### Downstream Trajectory Comparison
|
| 45 |
+
|
| 46 |
+
<p align="center">
|
| 47 |
+
<img src="demo/VLA_Traj.png" alt="AutoVLA trajectory comparison under different image translation methods" width="1100"/>
|
| 48 |
+
</p>
|
| 49 |
+
<p align="center">
|
| 50 |
+
<em>Qualitative comparison of AutoVLA rollouts using daylight reference images and images produced by different translation methods.</em>
|
| 51 |
+
</p>
|
| 52 |
+
|
| 53 |
+
## Installation and Requirements
|
| 54 |
+
|
| 55 |
+
The supplied inference code uses CUDA and imports FlashAttention directly. Use a compatible NVIDIA GPU and a Linux environment with the CUDA toolkit required by your FlashAttention build.
|
| 56 |
+
|
| 57 |
+
| Component | Requirement |
|
| 58 |
+
| --- | --- |
|
| 59 |
+
| Python | 3.10 or later |
|
| 60 |
+
| PyTorch | 2.6.0 or later, as specified in `requirements.txt` |
|
| 61 |
+
| torchvision | A version compatible with the installed PyTorch release; `requirements.txt` specifies 0.19.0 or later |
|
| 62 |
+
| FlashAttention | 2.0.0 or later, with a build compatible with PyTorch, CUDA, and the GPU |
|
| 63 |
+
| Default inference precision | `torch.bfloat16` |
|
| 64 |
+
| Default image resolution | 384 Γ 384 |
|
| 65 |
+
|
| 66 |
+
### 1. Clone the Repository
|
| 67 |
+
|
| 68 |
+
```shell
|
| 69 |
+
git clone https://github.com/val-utehy/RotEx.git
|
| 70 |
+
cd RotEx
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
All commands below run from the directory containing `config.py`, `flux/`, and the inference scripts. If using this code inside the BenchRotEx workspace, that directory is `B/RotEx`.
|
| 74 |
+
|
| 75 |
+
### 2. Create an Environment
|
| 76 |
+
|
| 77 |
+
```shell
|
| 78 |
+
conda create -n rotex python=3.10 -y
|
| 79 |
+
conda activate rotex
|
| 80 |
+
python -m pip install --upgrade pip
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### 3. Install PyTorch and Dependencies
|
| 84 |
+
|
| 85 |
+
Install a matching CUDA-enabled PyTorch and torchvision pair using the [official PyTorch installation instructions](https://pytorch.org/get-started/locally/). Ensure the selected versions satisfy the requirements above.
|
| 86 |
+
|
| 87 |
+
After installing PyTorch:
|
| 88 |
+
|
| 89 |
+
```shell
|
| 90 |
+
python -m pip install packaging ninja psutil
|
| 91 |
+
python -m pip install "flash-attn>=2.0.0" --no-build-isolation
|
| 92 |
+
python -m pip install -r requirements.txt
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
See the [FlashAttention installation instructions](https://github.com/Dao-AILab/flash-attention#installation-and-features) for supported CUDA/GPU combinations and build requirements.
|
| 96 |
+
|
| 97 |
+
Verify the environment:
|
| 98 |
+
|
| 99 |
+
```shell
|
| 100 |
+
python -c "import torch, flash_attn; print('PyTorch:', torch.__version__); print('CUDA:', torch.cuda.is_available()); print('BF16:', torch.cuda.is_bf16_supported())"
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### 4. Prepare the Auxiliary Models
|
| 104 |
+
|
| 105 |
+
The inference scripts load these pretrained components:
|
| 106 |
+
|
| 107 |
+
| Component | Source | Local behavior |
|
| 108 |
+
| --- | --- | --- |
|
| 109 |
+
| FLUX autoencoder | [black-forest-labs/FLUX.1-dev](https://huggingface.co/black-forest-labs/FLUX.1-dev), `ae.safetensors` | Loaded from `checkpoints/ae.safetensors`; downloaded there if missing |
|
| 110 |
+
| DINOv2 Base | [facebook/dinov2-base](https://huggingface.co/facebook/dinov2-base) | Downloaded through Transformers and stored in the Hugging Face cache |
|
| 111 |
+
| Depth Anything V2 Small | [depth-anything/Depth-Anything-V2-Small-hf](https://huggingface.co/depth-anything/Depth-Anything-V2-Small-hf) | Downloaded through Transformers and stored in the Hugging Face cache |
|
| 112 |
+
|
| 113 |
+
An internet connection is needed for the first download. If access to an auxiliary model requires authentication or approval, complete the access process on its model page and log in with `hf auth login`.
|
| 114 |
+
|
| 115 |
+
To download the autoencoder explicitly:
|
| 116 |
+
|
| 117 |
+
```shell
|
| 118 |
+
hf download black-forest-labs/FLUX.1-dev ae.safetensors --local-dir checkpoints
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## Pretrained Models
|
| 122 |
+
|
| 123 |
+
RotEx weights will be distributed through [Hugging Face](https://huggingface.co/val-utehy/RotEx). Download the checkpoint matching the inference mode you intend to run.
|
| 124 |
+
|
| 125 |
+
| Checkpoint | Purpose | Expected local path |
|
| 126 |
+
| --- | --- | --- |
|
| 127 |
+
| Full model, best checkpoint | Multi-step image and video inference | `checkpoints/full/best.pt` |
|
| 128 |
+
| Full model, final checkpoint | Alternative full-model checkpoint | `checkpoints/full/last.pt` |
|
| 129 |
+
| Distilled model, best checkpoint | One-step inference | `distill_checkpoints/best_distill.pt` |
|
| 130 |
+
|
| 131 |
+
Once the model repository is published with this directory structure, download the required weights from the project root:
|
| 132 |
+
|
| 133 |
+
```shell
|
| 134 |
+
hf download val-utehy/RotEx checkpoints/full/best.pt --local-dir .
|
| 135 |
+
hf download val-utehy/RotEx distill_checkpoints/best_distill.pt --local-dir .
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
The final full-model checkpoint is optional:
|
| 139 |
+
|
| 140 |
+
```shell
|
| 141 |
+
hf download val-utehy/RotEx checkpoints/full/last.pt --local-dir .
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
The `hf` command is provided by `huggingface_hub`. See the [Hugging Face CLI guide](https://huggingface.co/docs/huggingface_hub/guides/cli) for download and authentication options.
|
| 145 |
+
|
| 146 |
+
Additional placement instructions are available in [checkpoints/README.md](checkpoints/README.md), [checkpoints/full/README.md](checkpoints/full/README.md), and [distill_checkpoints/README.md](distill_checkpoints/README.md).
|
| 147 |
+
|
| 148 |
+
> [!NOTE]
|
| 149 |
+
> The scripts still contain paths from the original development environment. Pass explicit paths to the multi-step scripts and update the configuration constants in `infer_1step_style.py`. The model configuration is defined in `config.py`; these scripts do not use an `opt.pkl` file.
|
| 150 |
+
|
| 151 |
+
## Usage Guide
|
| 152 |
+
|
| 153 |
+
### 1. Model Training
|
| 154 |
+
|
| 155 |
+
This release contains model components and inference scripts. A standalone training entry point and a distillation training entry point are not included in this directory.
|
| 156 |
+
|
| 157 |
+
### 2. Choosing an Inference Script
|
| 158 |
+
|
| 159 |
+
| Script | Checkpoint | Style input | Output |
|
| 160 |
+
| --- | --- | --- | --- |
|
| 161 |
+
| `infer_1step_style.py` | Distilled | One fixed reference image | One PNG per image and hour for each configured input/output pair; hours 1β24 by default |
|
| 162 |
+
| `infer_multistep.py` | Full | Every image in a style directory | One labeled strip per content/style pair; hours 0β24 by default |
|
| 163 |
+
| `infer_multistep_vid.py` | Full | One fixed reference image | One MP4 with the target hour interpolated across frames |
|
| 164 |
+
|
| 165 |
+
All image-directory scripts read `.jpg`, `.jpeg`, and `.png` files directly inside the configured folders. They do not search subdirectories recursively.
|
| 166 |
+
|
| 167 |
+
### 3. Multi-Step Image Inference
|
| 168 |
+
|
| 169 |
+
Prepare separate content and style directories, for example:
|
| 170 |
+
|
| 171 |
+
```text
|
| 172 |
+
inputs/
|
| 173 |
+
βββ content/
|
| 174 |
+
β βββ scene_001.png
|
| 175 |
+
β βββ scene_002.jpg
|
| 176 |
+
βββ style/
|
| 177 |
+
βββ night_reference.jpg
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
Run:
|
| 181 |
+
|
| 182 |
+
```shell
|
| 183 |
+
python infer_multistep.py \
|
| 184 |
+
--checkpoint checkpoints/full/best.pt \
|
| 185 |
+
--content_dir inputs/content \
|
| 186 |
+
--style_dir inputs/style \
|
| 187 |
+
--output_dir outputs/multistep \
|
| 188 |
+
--denoise_steps 50 \
|
| 189 |
+
--image_size 384 \
|
| 190 |
+
--depth_size 48 \
|
| 191 |
+
--num_seeds 1 \
|
| 192 |
+
--device cuda \
|
| 193 |
+
--dtype bfloat16
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
Each content image is combined with every style image. Results are saved as `<content_stem>__sty_<style_stem>.png`. Each result is a horizontal strip containing the content image, style reference, depth visualization, and generated images for hours 0 through 24. Existing output files are skipped.
|
| 197 |
+
|
| 198 |
+
| Argument | Default | Description |
|
| 199 |
+
| --- | --- | --- |
|
| 200 |
+
| `--denoise_steps` | `50` | Number of Heun integration steps; each step evaluates the model twice |
|
| 201 |
+
| `--image_size` | `384` | Square image resolution used for content and style preprocessing |
|
| 202 |
+
| `--depth_size` | `48` | Depth-map resolution supplied to the model |
|
| 203 |
+
| `--num_seeds` | `1` | Number of independently sampled outputs to average per hour, in decoded image space |
|
| 204 |
+
| `--device` | `cuda` | Device used for inference |
|
| 205 |
+
| `--dtype` | `bfloat16` | Use `bfloat16` for the supplied default GPU path |
|
| 206 |
+
|
| 207 |
+
The hour list is set inside `run_inference()` and is not exposed as a command-line argument. The legacy `--noise_alpha` argument is unused; initialization is sampled from Gaussian noise. The image solver clamps latents to `[-20, 20]` during integration and `[-15, 15]` before decoding.
|
| 208 |
+
|
| 209 |
+
### 4. One-Step Image Inference with a Fixed Style
|
| 210 |
+
|
| 211 |
+
Open `infer_1step_style.py` and configure the distilled checkpoint, style image, and dataset paths:
|
| 212 |
+
|
| 213 |
+
```python
|
| 214 |
+
CHECKPOINT = "distill_checkpoints/best_distill.pt"
|
| 215 |
+
FIXED_STYLE_PATH = "inputs/style/night_reference.jpg"
|
| 216 |
+
DATA_PAIRS = [
|
| 217 |
+
{"input": "inputs/content", "output": "outputs/1step_style"},
|
| 218 |
+
]
|
| 219 |
+
HOURS = list(range(1, 25))
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
Run:
|
| 223 |
+
|
| 224 |
+
```shell
|
| 225 |
+
python infer_1step_style.py
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
The reference style is encoded once and reused for all configured image folders. Outputs follow the `<image_stem>_hXX.png` naming convention. The script writes to the configured output paths and can overwrite existing images with the same names.
|
| 229 |
+
|
| 230 |
+
### 5. Multi-Step Video Inference
|
| 231 |
+
|
| 232 |
+
```shell
|
| 233 |
+
python infer_multistep_vid.py \
|
| 234 |
+
--checkpoint checkpoints/full/best.pt \
|
| 235 |
+
--video_path inputs/video.mp4 \
|
| 236 |
+
--style_path inputs/style/night_reference.jpg \
|
| 237 |
+
--output_dir outputs/video \
|
| 238 |
+
--denoise_steps 50 \
|
| 239 |
+
--image_size 384 \
|
| 240 |
+
--depth_size 48 \
|
| 241 |
+
--max_height 384 \
|
| 242 |
+
--hour_start 0 \
|
| 243 |
+
--hour_end 24 \
|
| 244 |
+
--device cuda \
|
| 245 |
+
--dtype bfloat16 \
|
| 246 |
+
--no_audio
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
The script encodes the style reference once, reuses a base noise tensor across frames, and interpolates the target hour from `--hour_start` to `--hour_end` over the input video. The output is named `<video_stem>_chronflux.mp4` and uses the input video's reported frame rate.
|
| 250 |
+
|
| 251 |
+
`--image_size` controls the square resolution used for model inference. `--max_height` controls output resizing, with output dimensions aligned to multiples of 16. The video solver uses latent clamps of `[-8, 8]` during integration and `[-6, 6]` before decoding, which differ from the image solver.
|
| 252 |
+
|
| 253 |
+
The example produces a silent video. Audio transfer is optional and requires MoviePy, which is not included in `requirements.txt`; the script falls back to saving the video without audio if transfer fails.
|
| 254 |
+
|
| 255 |
+
### 6. Illumination Control and Reproducibility
|
| 256 |
+
|
| 257 |
+
The scripts map hours to the model's illumination angle as follows:
|
| 258 |
+
|
| 259 |
+
```python
|
| 260 |
+
phi = hour * (2 * math.pi / 24.0)
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
Hours 0 and 24 correspond to the endpoints of one full cycle. The image scripts use discrete hour lists, while the video script supplies intermediate hour values across frames. To render fractional hours in an image script, edit its hour list; the one-step filename formatting must also be adapted if the values are not integers.
|
| 264 |
+
|
| 265 |
+
The image scripts sample new Gaussian noise for each output and do not expose a seed argument. Repeated runs can therefore produce different results. Increasing `--num_seeds` in the multi-step image script averages decoded outputs and increases computation; it is separate from increasing the number of solver steps.
|
| 266 |
+
|
| 267 |
+
## Repository Structure
|
| 268 |
+
|
| 269 |
+
```text
|
| 270 |
+
RotEx/
|
| 271 |
+
βββ README.md
|
| 272 |
+
βββ config.py
|
| 273 |
+
βββ requirements.txt
|
| 274 |
+
βββ infer_1step_style.py
|
| 275 |
+
βββ infer_multistep.py
|
| 276 |
+
βββ infer_multistep_vid.py
|
| 277 |
+
βββ flux/ # Model, autoencoder, encoders, and utilities
|
| 278 |
+
βββ demo/ # Architecture and qualitative figures
|
| 279 |
+
βββ checkpoints/
|
| 280 |
+
β βββ README.md
|
| 281 |
+
β βββ ae.safetensors # Download separately
|
| 282 |
+
β βββ full/
|
| 283 |
+
β βββ README.md
|
| 284 |
+
β βββ best.pt # Download from the RotEx model repository
|
| 285 |
+
β βββ last.pt # Optional alternative full checkpoint
|
| 286 |
+
βββ distill_checkpoints/
|
| 287 |
+
βββ README.md
|
| 288 |
+
βββ best_distill.pt # Download from the RotEx model repository
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
The Python implementation retains internal names such as `ChronFlux` and the `q1_paper` configuration preset. These are the identifiers used by the current inference scripts.
|
| 292 |
+
|
| 293 |
+
## Citation
|
| 294 |
+
|
| 295 |
+
Paper details and the official BibTeX entry will be added when available.
|
| 296 |
+
|
| 297 |
+
## References
|
| 298 |
+
|
| 299 |
+
- [FLUX](https://github.com/black-forest-labs/flux)
|
| 300 |
+
- [DINOv2](https://github.com/facebookresearch/dinov2)
|
| 301 |
+
- [Depth Anything V2](https://github.com/DepthAnything/Depth-Anything-V2)
|
| 302 |
+
- [FlashAttention](https://github.com/Dao-AILab/flash-attention)
|
| 303 |
+
|
| 304 |
+
## License
|
| 305 |
+
|
| 306 |
+
A project license has not yet been included in this release. The license will be specified when the repository is published. Consult the respective upstream repositories and model cards for the terms governing pretrained components.
|
checkpoints/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pretrained Checkpoints
|
| 2 |
+
|
| 3 |
+
RotEx model weights will be hosted on **Hugging Face**:
|
| 4 |
+
|
| 5 |
+
[RotEx model repository](https://huggingface.co/val-utehy/RotEx)
|
| 6 |
+
|
| 7 |
+
The commands below assume the uploaded files preserve the directory structure shown here.
|
| 8 |
+
|
| 9 |
+
## Directory Layout
|
| 10 |
+
|
| 11 |
+
```text
|
| 12 |
+
checkpoints/
|
| 13 |
+
βββ README.md
|
| 14 |
+
βββ ae.safetensors
|
| 15 |
+
βββ full/
|
| 16 |
+
βββ README.md
|
| 17 |
+
βββ best.pt
|
| 18 |
+
βββ last.pt
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
`best.pt` and `last.pt` are full-model checkpoints for `infer_multistep.py` and `infer_multistep_vid.py`. Use the distilled model in [`../distill_checkpoints/`](../distill_checkpoints/README.md) for `infer_1step_style.py`.
|
| 22 |
+
|
| 23 |
+
## Download the Full Model
|
| 24 |
+
|
| 25 |
+
Run from the project root, the directory containing `config.py`:
|
| 26 |
+
|
| 27 |
+
```shell
|
| 28 |
+
hf download val-utehy/RotEx checkpoints/full/best.pt --local-dir .
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Optionally download the final full-model checkpoint:
|
| 32 |
+
|
| 33 |
+
```shell
|
| 34 |
+
hf download val-utehy/RotEx checkpoints/full/last.pt --local-dir .
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Pass `--checkpoint checkpoints/full/best.pt` to either multi-step inference script. See [`full/README.md`](full/README.md) for examples.
|
| 38 |
+
|
| 39 |
+
## Download the Autoencoder
|
| 40 |
+
|
| 41 |
+
The FLUX autoencoder is distributed separately by its original provider:
|
| 42 |
+
|
| 43 |
+
[black-forest-labs/FLUX.1-dev](https://huggingface.co/black-forest-labs/FLUX.1-dev)
|
| 44 |
+
|
| 45 |
+
```shell
|
| 46 |
+
hf download black-forest-labs/FLUX.1-dev ae.safetensors --local-dir checkpoints
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
If authentication is required, complete the model's access process and run `hf auth login` first. The inference loader also attempts to download `ae.safetensors` automatically when it is absent from this directory.
|
| 50 |
+
|
| 51 |
+
DINOv2 and Depth Anything V2 are loaded through Transformers into the Hugging Face cache; their weights do not need to be copied into this folder.
|
| 52 |
+
|
| 53 |
+
See the [main README](../README.md) for installation and inference instructions.
|
checkpoints/full/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Full-Model Weights
|
| 2 |
+
|
| 3 |
+
Download full-model checkpoints from the [RotEx Hugging Face repository](https://huggingface.co/val-utehy/RotEx).
|
| 4 |
+
|
| 5 |
+
Downloads become available after the files are uploaded.
|
| 6 |
+
|
| 7 |
+
| File | Use |
|
| 8 |
+
| --- | --- |
|
| 9 |
+
| `best.pt` | Best full-model checkpoint |
|
| 10 |
+
| `last.pt` | Final full-model checkpoint; optional alternative |
|
| 11 |
+
|
| 12 |
+
Run from the project root:
|
| 13 |
+
|
| 14 |
+
```shell
|
| 15 |
+
hf download val-utehy/RotEx checkpoints/full/best.pt --local-dir .
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
To download the alternative checkpoint:
|
| 19 |
+
|
| 20 |
+
```shell
|
| 21 |
+
hf download val-utehy/RotEx checkpoints/full/last.pt --local-dir .
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The expected local paths are `checkpoints/full/best.pt` and `checkpoints/full/last.pt`.
|
| 25 |
+
|
| 26 |
+
## Image Inference
|
| 27 |
+
|
| 28 |
+
```shell
|
| 29 |
+
python infer_multistep.py \
|
| 30 |
+
--checkpoint checkpoints/full/best.pt \
|
| 31 |
+
--content_dir inputs/content \
|
| 32 |
+
--style_dir inputs/style \
|
| 33 |
+
--output_dir outputs/multistep
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Video Inference
|
| 37 |
+
|
| 38 |
+
```shell
|
| 39 |
+
python infer_multistep_vid.py \
|
| 40 |
+
--checkpoint checkpoints/full/best.pt \
|
| 41 |
+
--video_path inputs/video.mp4 \
|
| 42 |
+
--style_path inputs/style/night_reference.jpg \
|
| 43 |
+
--output_dir outputs/video \
|
| 44 |
+
--no_audio
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
These checkpoints are intended for the multi-step scripts. For one-step inference, download `distill_checkpoints/best_distill.pt` and follow the [distilled-checkpoint instructions](../../distill_checkpoints/README.md).
|
| 48 |
+
|
| 49 |
+
See the [main README](../../README.md) for dependencies, auxiliary models, and inference options.
|
checkpoints/full/best.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b24f810ca16eb2a6d1ab5eed60d5ea0ba9ead1d0ebeb827e1daaf77305670c7b
|
| 3 |
+
size 8396220961
|
checkpoints/full/last.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b24f810ca16eb2a6d1ab5eed60d5ea0ba9ead1d0ebeb827e1daaf77305670c7b
|
| 3 |
+
size 8396220961
|
config.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration system for enhanced RIFT model.
|
| 3 |
+
"""
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from typing import Literal
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class EncoderConfig:
|
| 9 |
+
type: Literal["siglip", "dinov2", "evaclip"] = "dinov2"
|
| 10 |
+
model_size: Literal["small", "base", "large"] = "base"
|
| 11 |
+
output_dim: int = 512
|
| 12 |
+
depth_aware: bool = False
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class LossConfig:
|
| 16 |
+
"""
|
| 17 |
+
Configuration for loss functions and weights.
|
| 18 |
+
"""
|
| 19 |
+
recon_weight: float = 10.0 # Charbonnier Loss
|
| 20 |
+
color_match_weight: float = 5.0 # ColorMatchingLoss
|
| 21 |
+
day_preserve_weight: float = 3.0 # DayPreservationLoss
|
| 22 |
+
moe_weight: float = 0.05 # MoE Load Balancing
|
| 23 |
+
brightness_cont_weight: float = 0.1
|
| 24 |
+
color_cont_weight: float = 0.1
|
| 25 |
+
|
| 26 |
+
use_charbonnier: bool = True
|
| 27 |
+
|
| 28 |
+
use_vgg: bool = True
|
| 29 |
+
vgg_weight: float = 0.5
|
| 30 |
+
# Tuning guide:
|
| 31 |
+
# 0.1 β subtle sharpening, safe
|
| 32 |
+
# 0.5 β noticeable edge/texture improvement β default
|
| 33 |
+
|
| 34 |
+
use_lpips: bool = False
|
| 35 |
+
lpips_weight: float = 0.0
|
| 36 |
+
|
| 37 |
+
use_l1: bool = False
|
| 38 |
+
use_gan: bool = False
|
| 39 |
+
gan_weight: float = 0.0
|
| 40 |
+
|
| 41 |
+
# Legacy (disabled)
|
| 42 |
+
use_smooth_transition: bool = False
|
| 43 |
+
smooth_weight: float = 0.0
|
| 44 |
+
use_content_preservation: bool = False
|
| 45 |
+
content_weight: float = 0.0
|
| 46 |
+
use_brightness_preservation: bool = False
|
| 47 |
+
brightness_weight: float = 0.0
|
| 48 |
+
min_brightness: float = 0.0
|
| 49 |
+
max_brightness: float = 0.0
|
| 50 |
+
charbonnier_eps: float = 1e-4
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class AttentionConfig:
|
| 54 |
+
use_cross_attention: bool = True
|
| 55 |
+
use_adain: bool = True
|
| 56 |
+
use_depth_attention: bool = True
|
| 57 |
+
use_multi_scale_attention: bool = False
|
| 58 |
+
|
| 59 |
+
@dataclass
|
| 60 |
+
class MoEConfig:
|
| 61 |
+
enabled: bool = True
|
| 62 |
+
num_experts: int = 4
|
| 63 |
+
top_k: int = 2
|
| 64 |
+
temperature: float = 0.3
|
| 65 |
+
noise_std: float = 0.1
|
| 66 |
+
load_balance_coef: float = 0.01
|
| 67 |
+
|
| 68 |
+
@dataclass
|
| 69 |
+
class SamplingConfig:
|
| 70 |
+
method: Literal["euler", "heun", "consistency"] = "heun"
|
| 71 |
+
num_steps: int = 30
|
| 72 |
+
use_flow_matching: bool = True
|
| 73 |
+
use_consistency: bool = False
|
| 74 |
+
use_cfg: bool = True
|
| 75 |
+
cfg_scale: float = 7.5
|
| 76 |
+
sigma_min: float = 0.002
|
| 77 |
+
sigma_max: float = 80.0
|
| 78 |
+
|
| 79 |
+
@dataclass
|
| 80 |
+
class ModelConfig:
|
| 81 |
+
in_channels: int = 16
|
| 82 |
+
hidden_size: int = 1024
|
| 83 |
+
num_double_layers: int = 8
|
| 84 |
+
num_single_layers: int = 16
|
| 85 |
+
num_heads: int = 16
|
| 86 |
+
style_dim: int = 512
|
| 87 |
+
use_text: bool = False
|
| 88 |
+
text_dim: int = 1152
|
| 89 |
+
encoder: EncoderConfig = field(default_factory=EncoderConfig)
|
| 90 |
+
loss: LossConfig = field(default_factory=LossConfig)
|
| 91 |
+
attention: AttentionConfig = field(default_factory=AttentionConfig)
|
| 92 |
+
moe: MoEConfig = field(default_factory=MoEConfig)
|
| 93 |
+
sampling: SamplingConfig = field(default_factory=SamplingConfig)
|
| 94 |
+
|
| 95 |
+
@dataclass
|
| 96 |
+
class TrainingConfig:
|
| 97 |
+
content_dir: str = "/workspace/ChronFLUX/rafael_ds_ver2/train/day"
|
| 98 |
+
style_dir: str = "/workspace/ChronFLUX/rafael_ds_ver2/train/night"
|
| 99 |
+
image_size: int = 384
|
| 100 |
+
batch_size: int = 1
|
| 101 |
+
grad_accum_steps: int = 4
|
| 102 |
+
epochs: int = 43
|
| 103 |
+
lr_max: float = 1e-7
|
| 104 |
+
lr_min: float = 1e-5
|
| 105 |
+
weight_decay: float = 1e-4
|
| 106 |
+
save_interval_epochs: int = 1
|
| 107 |
+
device: str = "cuda"
|
| 108 |
+
dtype: str = "bfloat16"
|
| 109 |
+
|
| 110 |
+
def get_q1_paper_config() -> ModelConfig:
|
| 111 |
+
return ModelConfig()
|
| 112 |
+
|
| 113 |
+
PRESETS = {
|
| 114 |
+
"q1_paper": get_q1_paper_config,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
def get_config(preset: str = "q1_paper") -> ModelConfig:
|
| 118 |
+
return PRESETS.get(preset, get_q1_paper_config)()
|
demo/RotEx_Proposed.png
ADDED
|
Git LFS Details
|
demo/VLA_Traj.png
ADDED
|
Git LFS Details
|
demo/illumination_variation_space.jpg
ADDED
|
Git LFS Details
|
distill_checkpoints/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Distilled One-Step Weights
|
| 2 |
+
|
| 3 |
+
Download the distilled checkpoint from the [RotEx Hugging Face repository](https://huggingface.co/val-utehy/RotEx).
|
| 4 |
+
|
| 5 |
+
Downloads become available after the checkpoint is uploaded.
|
| 6 |
+
|
| 7 |
+
Run from the project root, the directory containing `config.py`:
|
| 8 |
+
|
| 9 |
+
```shell
|
| 10 |
+
hf download val-utehy/RotEx distill_checkpoints/best_distill.pt --local-dir .
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
The expected local path is:
|
| 14 |
+
|
| 15 |
+
```text
|
| 16 |
+
distill_checkpoints/best_distill.pt
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
## Usage
|
| 20 |
+
|
| 21 |
+
Set the checkpoint constant in `infer_1step_style.py`:
|
| 22 |
+
|
| 23 |
+
```python
|
| 24 |
+
CHECKPOINT = "distill_checkpoints/best_distill.pt"
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
Configure the input/output paths in the script, then run:
|
| 28 |
+
|
| 29 |
+
```shell
|
| 30 |
+
python infer_1step_style.py
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
`infer_1step_style.py` requires a fixed reference image configured through `FIXED_STYLE_PATH` and input/output folders configured through `DATA_PAIRS`.
|
| 34 |
+
|
| 35 |
+
The script also needs the FLUX autoencoder, DINOv2, and Depth Anything V2 described in the [main README](../README.md). Full-model weights for multi-step inference belong in [`../checkpoints/full/`](../checkpoints/full/README.md).
|
distill_checkpoints/best_distill.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee1b4ef1320479571288b9f00cbd4ddd89e6e7829e0f9d1d104546f821d487ab
|
| 3 |
+
size 2798766921
|
flux/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from ._version import (
|
| 3 |
+
version as __version__,
|
| 4 |
+
version_tuple,
|
| 5 |
+
)
|
| 6 |
+
except ImportError:
|
| 7 |
+
__version__ = "unknown"
|
| 8 |
+
version_tuple = (0, 0, "unknown", "noinfo")
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
PACKAGE = __package__.replace("_", "-")
|
| 13 |
+
PACKAGE_ROOT = Path(__file__).parent
|
flux/chronflux.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
from einops import rearrange, repeat
|
| 4 |
+
from flux.modules.layers import EmbedND, timestep_embedding, MLPEmbedder
|
| 5 |
+
from flux.utils.math import attention, apply_rope
|
| 6 |
+
from flux.modules.attention import AdaIN1d, DepthControlledAttention
|
| 7 |
+
from flux.modules.moe import MoEStyleAdapter, MoEFeatureProcessor
|
| 8 |
+
import torch.utils.checkpoint as checkpoint
|
| 9 |
+
from typing import Optional, Dict, Any
|
| 10 |
+
|
| 11 |
+
class SwiGLU(nn.Module):
|
| 12 |
+
def __init__(self, in_features, hidden_features=None, out_features=None):
|
| 13 |
+
super().__init__()
|
| 14 |
+
out_features = out_features or in_features
|
| 15 |
+
hidden_features = hidden_features or in_features
|
| 16 |
+
hidden_features = int(hidden_features * 2 / 3)
|
| 17 |
+
self.w1 = nn.Linear(in_features, hidden_features)
|
| 18 |
+
self.w2 = nn.Linear(in_features, hidden_features)
|
| 19 |
+
self.w3 = nn.Linear(hidden_features, out_features)
|
| 20 |
+
self.silu = nn.SiLU()
|
| 21 |
+
|
| 22 |
+
def forward(self, x):
|
| 23 |
+
x1 = self.w1(x)
|
| 24 |
+
x2 = self.w2(x)
|
| 25 |
+
hidden = self.silu(x1) * x2
|
| 26 |
+
return self.w3(hidden)
|
| 27 |
+
|
| 28 |
+
class AdaLNModulation(nn.Module):
|
| 29 |
+
def __init__(self, hidden_size, components=6):
|
| 30 |
+
super().__init__()
|
| 31 |
+
self.silu = nn.SiLU()
|
| 32 |
+
self.linear = nn.Linear(hidden_size, components * hidden_size, bias=True)
|
| 33 |
+
self.components = components
|
| 34 |
+
nn.init.zeros_(self.linear.weight)
|
| 35 |
+
nn.init.zeros_(self.linear.bias)
|
| 36 |
+
|
| 37 |
+
def forward(self, vec):
|
| 38 |
+
chunk = self.linear(self.silu(vec))
|
| 39 |
+
return chunk.chunk(self.components, dim=1)
|
| 40 |
+
|
| 41 |
+
class QKNorm(nn.Module):
|
| 42 |
+
def __init__(self, dim: int):
|
| 43 |
+
super().__init__()
|
| 44 |
+
self.query_norm = nn.LayerNorm(dim, elementwise_affine=True, eps=1e-6)
|
| 45 |
+
self.key_norm = nn.LayerNorm(dim, elementwise_affine=True, eps=1e-6)
|
| 46 |
+
|
| 47 |
+
def forward(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 48 |
+
return self.query_norm(q), self.key_norm(k)
|
| 49 |
+
|
| 50 |
+
class ChronFluxDoubleStreamBlock(nn.Module):
|
| 51 |
+
def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, use_depth_attn=False):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.hidden_size = hidden_size
|
| 54 |
+
self.num_heads = num_heads
|
| 55 |
+
self.head_dim = hidden_size // num_heads
|
| 56 |
+
self.use_depth_attn = use_depth_attn
|
| 57 |
+
self.img_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 58 |
+
self.img_qkv = nn.Linear(hidden_size, hidden_size * 3)
|
| 59 |
+
self.img_proj = nn.Linear(hidden_size, hidden_size)
|
| 60 |
+
self.sty_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 61 |
+
self.sty_qkv = nn.Linear(hidden_size, hidden_size * 3)
|
| 62 |
+
self.sty_proj = nn.Linear(hidden_size, hidden_size)
|
| 63 |
+
self.img_qk_norm = QKNorm(self.head_dim)
|
| 64 |
+
self.sty_qk_norm = QKNorm(self.head_dim)
|
| 65 |
+
self.img_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 66 |
+
self.img_mlp = SwiGLU(hidden_size, int(hidden_size * mlp_ratio))
|
| 67 |
+
self.sty_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 68 |
+
self.sty_mlp = SwiGLU(hidden_size, int(hidden_size * mlp_ratio))
|
| 69 |
+
self.adaLN = AdaLNModulation(hidden_size, components=12)
|
| 70 |
+
if use_depth_attn:
|
| 71 |
+
self.depth_attn = DepthControlledAttention(hidden_size, num_heads=num_heads)
|
| 72 |
+
self.use_adain = False
|
| 73 |
+
|
| 74 |
+
def set_adain(self, use_adain, style_dim):
|
| 75 |
+
self.use_adain = use_adain
|
| 76 |
+
if use_adain:
|
| 77 |
+
self.adain = AdaIN1d(self.hidden_size, style_dim)
|
| 78 |
+
|
| 79 |
+
def forward(self, img, sty, vec, pe, depth_map=None, style_vec=None):
|
| 80 |
+
if self.use_adain and style_vec is not None:
|
| 81 |
+
adain_style = style_vec
|
| 82 |
+
if style_vec.dim() == 3:
|
| 83 |
+
adain_style = style_vec.mean(dim=1)
|
| 84 |
+
img = self.adain(img, adain_style)
|
| 85 |
+
|
| 86 |
+
params = self.adaLN(vec)
|
| 87 |
+
(shift_i_a, scale_i_a, gate_i_a, shift_i_m, scale_i_m, gate_i_m,
|
| 88 |
+
shift_s_a, scale_s_a, gate_s_a, shift_s_m, scale_s_m, gate_s_m) = params
|
| 89 |
+
|
| 90 |
+
img_mod = self.img_norm1(img) * (1 + scale_i_a.unsqueeze(1)) + shift_i_a.unsqueeze(1)
|
| 91 |
+
sty_mod = self.sty_norm1(sty) * (1 + scale_s_a.unsqueeze(1)) + shift_s_a.unsqueeze(1)
|
| 92 |
+
qkv_i = self.img_qkv(img_mod)
|
| 93 |
+
qkv_s = self.sty_qkv(sty_mod)
|
| 94 |
+
q_i, k_i, v_i = rearrange(qkv_i, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
|
| 95 |
+
q_s, k_s, v_s = rearrange(qkv_s, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
|
| 96 |
+
q_i, k_i = self.img_qk_norm(q_i, k_i)
|
| 97 |
+
q_s, k_s = self.sty_qk_norm(q_s, k_s)
|
| 98 |
+
dtype = img.dtype
|
| 99 |
+
q_i, k_i, v_i = q_i.to(dtype), k_i.to(dtype), v_i.to(dtype)
|
| 100 |
+
q_s, k_s, v_s = q_s.to(dtype), k_s.to(dtype), v_s.to(dtype)
|
| 101 |
+
L_s_tokens = sty.shape[1]
|
| 102 |
+
pe_s = pe[:, :, :L_s_tokens, ...]
|
| 103 |
+
pe_i = pe[:, :, L_s_tokens:, ...]
|
| 104 |
+
q_i, k_i = apply_rope(q_i, k_i, pe_i)
|
| 105 |
+
q_s, k_s = apply_rope(q_s, k_s, pe_s)
|
| 106 |
+
q_i, k_i = q_i.to(dtype), k_i.to(dtype)
|
| 107 |
+
q_s, k_s = q_s.to(dtype), k_s.to(dtype)
|
| 108 |
+
q_i = rearrange(q_i, "B H L D -> B L H D")
|
| 109 |
+
k_i = rearrange(k_i, "B H L D -> B L H D")
|
| 110 |
+
v_i = rearrange(v_i, "B H L D -> B L H D")
|
| 111 |
+
q_s = rearrange(q_s, "B H L D -> B L H D")
|
| 112 |
+
k_s = rearrange(k_s, "B H L D -> B L H D")
|
| 113 |
+
v_s = rearrange(v_s, "B H L D -> B L H D")
|
| 114 |
+
full_k = torch.cat([k_s, k_i], dim=1)
|
| 115 |
+
full_v = torch.cat([v_s, v_i], dim=1)
|
| 116 |
+
img_attn_out = self.img_proj(attention(q_i, full_k, full_v))
|
| 117 |
+
sty_attn_out = self.sty_proj(attention(q_s, full_k, full_v))
|
| 118 |
+
img = img + gate_i_a.unsqueeze(1) * img_attn_out
|
| 119 |
+
sty = sty + gate_s_a.unsqueeze(1) * sty_attn_out
|
| 120 |
+
if self.use_depth_attn and depth_map is not None:
|
| 121 |
+
img = img + self.depth_attn(img, depth_map)
|
| 122 |
+
img_mod = self.img_norm2(img) * (1 + scale_i_m.unsqueeze(1)) + shift_i_m.unsqueeze(1)
|
| 123 |
+
img = img + gate_i_m.unsqueeze(1) * self.img_mlp(img_mod)
|
| 124 |
+
sty_mod = self.sty_norm2(sty) * (1 + scale_s_m.unsqueeze(1)) + shift_s_m.unsqueeze(1)
|
| 125 |
+
sty = sty + gate_s_m.unsqueeze(1) * self.sty_mlp(sty_mod)
|
| 126 |
+
return img, sty
|
| 127 |
+
|
| 128 |
+
class ChronFluxSingleStreamBlock(nn.Module):
|
| 129 |
+
def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, use_moe=False, moe_params: Optional[Dict[str, Any]] = None):
|
| 130 |
+
super().__init__()
|
| 131 |
+
self.hidden_size = hidden_size
|
| 132 |
+
self.num_heads = num_heads
|
| 133 |
+
self.head_dim = hidden_size // num_heads
|
| 134 |
+
self.use_moe = use_moe
|
| 135 |
+
self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 136 |
+
self.qkv = nn.Linear(hidden_size, hidden_size * 3)
|
| 137 |
+
self.proj = nn.Linear(hidden_size, hidden_size)
|
| 138 |
+
self.qk_norm = QKNorm(self.head_dim)
|
| 139 |
+
self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 140 |
+
if use_moe:
|
| 141 |
+
params = moe_params or {}
|
| 142 |
+
num_experts = params.get('num_experts', 3) # Default safe value
|
| 143 |
+
# Extract only valid args for MoEFeatureProcessor
|
| 144 |
+
valid_keys = ['custom_centers', 'custom_widths']
|
| 145 |
+
kwargs = {k: params[k] for k in valid_keys if k in params}
|
| 146 |
+
|
| 147 |
+
self.moe_processor = MoEFeatureProcessor(hidden_size, num_experts=num_experts, **kwargs)
|
| 148 |
+
self.mlp = None
|
| 149 |
+
else:
|
| 150 |
+
self.mlp = SwiGLU(hidden_size, int(hidden_size * mlp_ratio))
|
| 151 |
+
self.moe_processor = None
|
| 152 |
+
self.adaLN = AdaLNModulation(hidden_size, components=6)
|
| 153 |
+
|
| 154 |
+
def forward(self, x, vec, pe, phi=None):
|
| 155 |
+
shift_a, scale_a, gate_a, shift_m, scale_m, gate_m = self.adaLN(vec)
|
| 156 |
+
x_mod = self.norm1(x) * (1 + scale_a.unsqueeze(1)) + shift_a.unsqueeze(1)
|
| 157 |
+
qkv = self.qkv(x_mod)
|
| 158 |
+
q, k, v = rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
|
| 159 |
+
q, k = self.qk_norm(q, k)
|
| 160 |
+
dtype = x.dtype
|
| 161 |
+
q, k, v = q.to(dtype), k.to(dtype), v.to(dtype)
|
| 162 |
+
q, k = apply_rope(q, k, pe)
|
| 163 |
+
q, k = q.to(dtype), k.to(dtype)
|
| 164 |
+
q = rearrange(q, "B H L D -> B L H D")
|
| 165 |
+
k = rearrange(k, "B H L D -> B L H D")
|
| 166 |
+
v = rearrange(v, "B H L D -> B L H D")
|
| 167 |
+
x_attn = self.proj(attention(q, k, v))
|
| 168 |
+
x = x + gate_a.unsqueeze(1) * x_attn
|
| 169 |
+
x_mod = self.norm2(x) * (1 + scale_m.unsqueeze(1)) + shift_m.unsqueeze(1)
|
| 170 |
+
|
| 171 |
+
aux_loss = 0.0
|
| 172 |
+
if self.use_moe and phi is not None:
|
| 173 |
+
moe_out, aux = self.moe_processor(x_mod, phi, return_aux=True)
|
| 174 |
+
x = x + gate_m.unsqueeze(1) * moe_out
|
| 175 |
+
if aux is not None:
|
| 176 |
+
aux_loss = aux['load_balance_loss']
|
| 177 |
+
else:
|
| 178 |
+
x = x + gate_m.unsqueeze(1) * self.mlp(x_mod)
|
| 179 |
+
|
| 180 |
+
return x, aux_loss
|
| 181 |
+
|
| 182 |
+
class ChronFlux(nn.Module):
|
| 183 |
+
def __init__(self, in_channels=16, hidden_size=1024, num_double_layers=8,
|
| 184 |
+
num_single_layers=16, num_heads=16, style_dim=512,
|
| 185 |
+
use_text=False, text_dim=1152,
|
| 186 |
+
use_moe=True, use_depth_attn=True, use_adain=False,
|
| 187 |
+
moe_params: Optional[Dict[str, Any]] = None):
|
| 188 |
+
super().__init__()
|
| 189 |
+
self.in_channels = in_channels
|
| 190 |
+
self.gradient_checkpointing = False
|
| 191 |
+
self.use_text = use_text
|
| 192 |
+
self.style_dim = style_dim
|
| 193 |
+
self.use_moe = use_moe
|
| 194 |
+
self.use_depth_attn = use_depth_attn
|
| 195 |
+
self.use_adain = use_adain
|
| 196 |
+
self.img_in = nn.Linear(16 * 3, hidden_size)
|
| 197 |
+
self.depth_proj = nn.Conv2d(1, 16, kernel_size=1)
|
| 198 |
+
self.style_in = nn.Linear(style_dim, hidden_size)
|
| 199 |
+
self.default_style = nn.Parameter(torch.randn(1, style_dim))
|
| 200 |
+
self.time_mlp = nn.Sequential(
|
| 201 |
+
nn.Linear(256, hidden_size),
|
| 202 |
+
nn.SiLU(),
|
| 203 |
+
nn.Linear(hidden_size, hidden_size)
|
| 204 |
+
)
|
| 205 |
+
if self.use_text:
|
| 206 |
+
self.txt_mlp = nn.Sequential(
|
| 207 |
+
nn.Linear(text_dim, hidden_size),
|
| 208 |
+
nn.SiLU(),
|
| 209 |
+
nn.Linear(hidden_size, hidden_size)
|
| 210 |
+
)
|
| 211 |
+
self.style_adapter = nn.Sequential(
|
| 212 |
+
nn.Linear(text_dim, hidden_size),
|
| 213 |
+
nn.SiLU(),
|
| 214 |
+
nn.Linear(hidden_size, style_dim)
|
| 215 |
+
)
|
| 216 |
+
nn.init.zeros_(self.style_adapter[-1].weight)
|
| 217 |
+
nn.init.zeros_(self.style_adapter[-1].bias)
|
| 218 |
+
self.phi_mlp = nn.Sequential(
|
| 219 |
+
nn.Linear(5, hidden_size),
|
| 220 |
+
nn.SiLU(),
|
| 221 |
+
nn.Linear(hidden_size, hidden_size)
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Pass MoE params
|
| 225 |
+
if self.use_moe:
|
| 226 |
+
params = moe_params or {}
|
| 227 |
+
num_experts = params.get('num_experts', 3)
|
| 228 |
+
valid_keys = ['custom_centers', 'custom_widths']
|
| 229 |
+
kwargs = {k: params[k] for k in valid_keys if k in params}
|
| 230 |
+
self.moe_style_adapter = MoEStyleAdapter(style_dim, hidden_dim=style_dim * 2, num_experts=num_experts, **kwargs)
|
| 231 |
+
|
| 232 |
+
self.vector_in = MLPEmbedder(hidden_size, hidden_size)
|
| 233 |
+
self.pe_embedder = EmbedND(dim=hidden_size // num_heads, theta=10000, axes_dim=[16, 24, 24])
|
| 234 |
+
self.double_blocks = nn.ModuleList()
|
| 235 |
+
for _ in range(num_double_layers):
|
| 236 |
+
block = ChronFluxDoubleStreamBlock(hidden_size, num_heads, use_depth_attn=use_depth_attn)
|
| 237 |
+
block.set_adain(use_adain, style_dim)
|
| 238 |
+
self.double_blocks.append(block)
|
| 239 |
+
self.single_blocks = nn.ModuleList([
|
| 240 |
+
ChronFluxSingleStreamBlock(
|
| 241 |
+
hidden_size, num_heads,
|
| 242 |
+
use_moe=(use_moe and i >= num_single_layers // 2),
|
| 243 |
+
moe_params=moe_params
|
| 244 |
+
)
|
| 245 |
+
for i in range(num_single_layers)
|
| 246 |
+
])
|
| 247 |
+
self.final_norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 248 |
+
self.final_layer = nn.Linear(hidden_size, in_channels)
|
| 249 |
+
|
| 250 |
+
def enable_gradient_checkpointing(self):
|
| 251 |
+
self.gradient_checkpointing = True
|
| 252 |
+
|
| 253 |
+
def forward(self, x_t, content_latent, depth_map, style_vec, timesteps, phi, txt_emb=None):
|
| 254 |
+
B, C, H, W = x_t.shape
|
| 255 |
+
moe_loss = torch.tensor(0.0, device=x_t.device, dtype=x_t.dtype)
|
| 256 |
+
|
| 257 |
+
content_normalized = content_latent
|
| 258 |
+
depth_small = torch.nn.functional.interpolate(depth_map, size=(H, W), mode="nearest")
|
| 259 |
+
depth_emb = self.depth_proj(depth_small)
|
| 260 |
+
img_cat = torch.cat([x_t, content_normalized, depth_emb], dim=1)
|
| 261 |
+
img_flat = rearrange(img_cat, 'b c h w -> b (h w) c')
|
| 262 |
+
img = self.img_in(img_flat)
|
| 263 |
+
|
| 264 |
+
if style_vec is None:
|
| 265 |
+
style_vec = self.default_style.expand(B, -1).to(x_t.dtype)
|
| 266 |
+
if self.use_text and txt_emb is not None:
|
| 267 |
+
style_delta = self.style_adapter(txt_emb)
|
| 268 |
+
style_vec = style_vec + style_delta
|
| 269 |
+
|
| 270 |
+
if self.use_moe:
|
| 271 |
+
style_vec, aux = self.moe_style_adapter(style_vec, phi, return_aux=True)
|
| 272 |
+
if aux is not None:
|
| 273 |
+
moe_loss += aux['load_balance_loss']
|
| 274 |
+
|
| 275 |
+
style = self.style_in(style_vec)
|
| 276 |
+
if style.dim() == 2:
|
| 277 |
+
style = style.unsqueeze(1)
|
| 278 |
+
L_s = style.shape[1]
|
| 279 |
+
|
| 280 |
+
t_emb = timestep_embedding(timesteps, 256).to(x_t.device).to(x_t.dtype)
|
| 281 |
+
t_vec = self.time_mlp(t_emb)
|
| 282 |
+
phi_norm = (phi % (2 * torch.pi)) / (2 * torch.pi)
|
| 283 |
+
phi_input = torch.cat([
|
| 284 |
+
torch.sin(phi.unsqueeze(1)),
|
| 285 |
+
torch.cos(phi.unsqueeze(1)),
|
| 286 |
+
torch.sin(2 * phi.unsqueeze(1)),
|
| 287 |
+
torch.cos(2 * phi.unsqueeze(1)),
|
| 288 |
+
phi_norm.unsqueeze(1)
|
| 289 |
+
], dim=1)
|
| 290 |
+
phi_vec = self.phi_mlp(phi_input)
|
| 291 |
+
vec_input = t_vec + phi_vec
|
| 292 |
+
if self.use_text and txt_emb is not None:
|
| 293 |
+
txt_vec = self.txt_mlp(txt_emb)
|
| 294 |
+
vec_input = vec_input + txt_vec
|
| 295 |
+
vec = self.vector_in(vec_input)
|
| 296 |
+
|
| 297 |
+
img_ids = torch.zeros(H, W, 3).to(x_t.device)
|
| 298 |
+
img_ids[..., 1] = img_ids[..., 1] + torch.arange(H, device=x_t.device)[:, None]
|
| 299 |
+
img_ids[..., 2] = img_ids[..., 2] + torch.arange(W, device=x_t.device)[None, :]
|
| 300 |
+
if self.training:
|
| 301 |
+
img_ids += torch.randn_like(img_ids) * 0.5
|
| 302 |
+
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=B)
|
| 303 |
+
pe = self.pe_embedder(img_ids)
|
| 304 |
+
dtype = pe.dtype
|
| 305 |
+
device = pe.device
|
| 306 |
+
pe_dim = pe.shape[-3]
|
| 307 |
+
style_pe = torch.zeros(B, 1, L_s, pe_dim, 2, 2, device=device, dtype=dtype)
|
| 308 |
+
full_pe = torch.cat([style_pe, pe], dim=2)
|
| 309 |
+
|
| 310 |
+
for block in self.double_blocks:
|
| 311 |
+
if self.gradient_checkpointing and self.training:
|
| 312 |
+
img, style = checkpoint.checkpoint(block, img, style, vec, full_pe, depth_map, style_vec, use_reentrant=False)
|
| 313 |
+
else:
|
| 314 |
+
img, style = block(img, style, vec, full_pe, depth_map, style_vec=style_vec)
|
| 315 |
+
|
| 316 |
+
x = torch.cat([style, img], dim=1)
|
| 317 |
+
|
| 318 |
+
for block in self.single_blocks:
|
| 319 |
+
if self.gradient_checkpointing and self.training:
|
| 320 |
+
x, block_loss = checkpoint.checkpoint(block, x, vec, full_pe, phi, use_reentrant=False)
|
| 321 |
+
else:
|
| 322 |
+
x, block_loss = block(x, vec, full_pe, phi)
|
| 323 |
+
moe_loss += block_loss
|
| 324 |
+
|
| 325 |
+
x = x[:, style.shape[1]:, :]
|
| 326 |
+
x = self.final_norm(x)
|
| 327 |
+
x = self.final_layer(x)
|
| 328 |
+
x = rearrange(x, 'b (h w) c -> b c h w', h=H, w=W)
|
| 329 |
+
|
| 330 |
+
return x, moe_loss
|
flux/modules/__init__.py
ADDED
|
File without changes
|
flux/modules/adaptive_blocks.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Adaptive Flux Blocks with enhanced AdaLN integration.
|
| 3 |
+
These blocks serve as building blocks for the SwiftFlux architecture with
|
| 4 |
+
advanced style transfer control and optional MoE-compatible components.
|
| 5 |
+
Author: https://github.com/datnguyen-tien204
|
| 6 |
+
"""
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
from einops import rearrange
|
| 10 |
+
from flux.modules.normalization import AdaLNModulation
|
| 11 |
+
from flux.utils.math import attention, apply_rope
|
| 12 |
+
from flux.utils.logger_utils import ConsoleLogger
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Constants
|
| 16 |
+
SWIGLU_HIDDEN_RATIO = 2 / 3 # Hidden dimension ratio for SwiGLU
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class QKNorm(nn.Module):
|
| 20 |
+
"""Query-Key normalization for stable attention"""
|
| 21 |
+
def __init__(self, dim: int):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.query_norm = nn.LayerNorm(dim, elementwise_affine=True, eps=1e-6)
|
| 24 |
+
self.key_norm = nn.LayerNorm(dim, elementwise_affine=True, eps=1e-6)
|
| 25 |
+
|
| 26 |
+
def forward(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 27 |
+
return self.query_norm(q), self.key_norm(k)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class SwiGLU(nn.Module):
|
| 31 |
+
"""SwiGLU activation for efficient FFN"""
|
| 32 |
+
def __init__(self, in_features, hidden_features=None, out_features=None):
|
| 33 |
+
super().__init__()
|
| 34 |
+
out_features = out_features or in_features
|
| 35 |
+
hidden_features = hidden_features or in_features
|
| 36 |
+
hidden_features = int(hidden_features * SWIGLU_HIDDEN_RATIO)
|
| 37 |
+
|
| 38 |
+
self.w1 = nn.Linear(in_features, hidden_features)
|
| 39 |
+
self.w2 = nn.Linear(in_features, hidden_features)
|
| 40 |
+
self.w3 = nn.Linear(hidden_features, out_features)
|
| 41 |
+
self.silu = nn.SiLU()
|
| 42 |
+
|
| 43 |
+
def forward(self, x):
|
| 44 |
+
x1 = self.w1(x)
|
| 45 |
+
x2 = self.w2(x)
|
| 46 |
+
hidden = self.silu(x1) * x2
|
| 47 |
+
return self.w3(hidden)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class AdaptiveFluxBlock(nn.Module):
|
| 51 |
+
"""
|
| 52 |
+
Base Adaptive Flux Block with AdaLN integration.
|
| 53 |
+
Serves as foundation for both double-stream and single-stream variants.
|
| 54 |
+
|
| 55 |
+
Features:
|
| 56 |
+
- AdaLN for layer-wise style and conditioning control
|
| 57 |
+
- QKNorm for attention stability
|
| 58 |
+
- SwiGLU for efficient feed-forward
|
| 59 |
+
- Optional sparse activation support for MoE compatibility
|
| 60 |
+
"""
|
| 61 |
+
def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, use_sparse=False):
|
| 62 |
+
super().__init__()
|
| 63 |
+
self.hidden_size = hidden_size
|
| 64 |
+
self.num_heads = num_heads
|
| 65 |
+
self.head_dim = hidden_size // num_heads
|
| 66 |
+
self.use_sparse = use_sparse
|
| 67 |
+
# Note: adaLN should be initialized in subclasses
|
| 68 |
+
|
| 69 |
+
def apply_adaptive_norm(self, x, scale, shift):
|
| 70 |
+
"""Apply AdaLN normalization with scale and shift"""
|
| 71 |
+
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class AdaptiveDoubleStreamBlock(AdaptiveFluxBlock):
|
| 75 |
+
"""
|
| 76 |
+
Adaptive Double-Stream Block with enhanced AdaLN.
|
| 77 |
+
Processes image and style streams separately with cross-attention.
|
| 78 |
+
|
| 79 |
+
Improvements:
|
| 80 |
+
- Dedicated AdaLN for both streams
|
| 81 |
+
- Enhanced normalization for better style consistency
|
| 82 |
+
- Support for sparse activation patterns
|
| 83 |
+
"""
|
| 84 |
+
def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, use_sparse=False):
|
| 85 |
+
super().__init__(hidden_size, num_heads, mlp_ratio, use_sparse)
|
| 86 |
+
|
| 87 |
+
# Image stream components
|
| 88 |
+
self.img_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 89 |
+
self.img_qkv = nn.Linear(hidden_size, hidden_size * 3)
|
| 90 |
+
self.img_proj = nn.Linear(hidden_size, hidden_size)
|
| 91 |
+
self.img_qk_norm = QKNorm(self.head_dim)
|
| 92 |
+
|
| 93 |
+
self.img_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 94 |
+
self.img_mlp = SwiGLU(hidden_size, int(hidden_size * mlp_ratio))
|
| 95 |
+
|
| 96 |
+
# Style stream components
|
| 97 |
+
self.sty_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 98 |
+
self.sty_qkv = nn.Linear(hidden_size, hidden_size * 3)
|
| 99 |
+
self.sty_proj = nn.Linear(hidden_size, hidden_size)
|
| 100 |
+
self.sty_qk_norm = QKNorm(self.head_dim)
|
| 101 |
+
|
| 102 |
+
self.sty_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 103 |
+
self.sty_mlp = SwiGLU(hidden_size, int(hidden_size * mlp_ratio))
|
| 104 |
+
|
| 105 |
+
# AdaLN with 12 components (6 for img, 6 for style)
|
| 106 |
+
self.adaLN = AdaLNModulation(hidden_size, components=12)
|
| 107 |
+
|
| 108 |
+
def forward(self, img, sty, vec, pe):
|
| 109 |
+
"""
|
| 110 |
+
Forward pass with dual-stream processing
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
img: Image features [B, L_img, D]
|
| 114 |
+
sty: Style features [B, L_sty, D]
|
| 115 |
+
vec: Conditioning vector [B, D]
|
| 116 |
+
pe: Position embeddings
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
Updated (img, sty) features
|
| 120 |
+
"""
|
| 121 |
+
# Generate all modulation parameters
|
| 122 |
+
params = self.adaLN(vec)
|
| 123 |
+
(shift_i_a, scale_i_a, gate_i_a, shift_i_m, scale_i_m, gate_i_m,
|
| 124 |
+
shift_s_a, scale_s_a, gate_s_a, shift_s_m, scale_s_m, gate_s_m) = params
|
| 125 |
+
|
| 126 |
+
# Image stream - attention
|
| 127 |
+
img_mod = self.apply_adaptive_norm(self.img_norm1(img), scale_i_a, shift_i_a)
|
| 128 |
+
sty_mod = self.apply_adaptive_norm(self.sty_norm1(sty), scale_s_a, shift_s_a)
|
| 129 |
+
|
| 130 |
+
qkv_i = self.img_qkv(img_mod)
|
| 131 |
+
qkv_s = self.sty_qkv(sty_mod)
|
| 132 |
+
|
| 133 |
+
q_i, k_i, v_i = rearrange(qkv_i, "B L (K H D) -> K B L H D", K=3, H=self.num_heads)
|
| 134 |
+
q_s, k_s, v_s = rearrange(qkv_s, "B L (K H D) -> K B L H D", K=3, H=self.num_heads)
|
| 135 |
+
|
| 136 |
+
q_i, k_i = self.img_qk_norm(q_i, k_i)
|
| 137 |
+
q_s, k_s = self.sty_qk_norm(q_s, k_s)
|
| 138 |
+
|
| 139 |
+
# Preserve dtype for Flash Attention
|
| 140 |
+
dtype = img.dtype
|
| 141 |
+
q_i, k_i, v_i = q_i.to(dtype), k_i.to(dtype), v_i.to(dtype)
|
| 142 |
+
q_s, k_s, v_s = q_s.to(dtype), k_s.to(dtype), v_s.to(dtype)
|
| 143 |
+
|
| 144 |
+
# Apply RoPE if position embeddings provided
|
| 145 |
+
if pe is not None:
|
| 146 |
+
pe = pe.transpose(1, 2)
|
| 147 |
+
pe_s = pe[:, :1, ...]
|
| 148 |
+
pe_i = pe[:, 1:, ...]
|
| 149 |
+
|
| 150 |
+
q_i, k_i = apply_rope(q_i, k_i, pe_i)
|
| 151 |
+
q_s, k_s = apply_rope(q_s, k_s, pe_s)
|
| 152 |
+
|
| 153 |
+
q_i, k_i = q_i.to(dtype), k_i.to(dtype)
|
| 154 |
+
q_s, k_s = q_s.to(dtype), k_s.to(dtype)
|
| 155 |
+
|
| 156 |
+
# Cross-attention between streams
|
| 157 |
+
full_k = torch.cat([k_s, k_i], dim=1)
|
| 158 |
+
full_v = torch.cat([v_s, v_i], dim=1)
|
| 159 |
+
|
| 160 |
+
img_attn_out = self.img_proj(attention(q_i, full_k, full_v))
|
| 161 |
+
sty_attn_out = self.sty_proj(attention(q_s, full_k, full_v))
|
| 162 |
+
|
| 163 |
+
# Apply gating and residual
|
| 164 |
+
img = img + gate_i_a.unsqueeze(1) * img_attn_out
|
| 165 |
+
sty = sty + gate_s_a.unsqueeze(1) * sty_attn_out
|
| 166 |
+
|
| 167 |
+
# MLP blocks with AdaLN
|
| 168 |
+
img_mod = self.apply_adaptive_norm(self.img_norm2(img), scale_i_m, shift_i_m)
|
| 169 |
+
img = img + gate_i_m.unsqueeze(1) * self.img_mlp(img_mod)
|
| 170 |
+
|
| 171 |
+
sty_mod = self.apply_adaptive_norm(self.sty_norm2(sty), scale_s_m, shift_s_m)
|
| 172 |
+
sty = sty + gate_s_m.unsqueeze(1) * self.sty_mlp(sty_mod)
|
| 173 |
+
|
| 174 |
+
return img, sty
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class AdaptiveSingleStreamBlock(AdaptiveFluxBlock):
|
| 178 |
+
"""
|
| 179 |
+
Adaptive Single-Stream Block with enhanced AdaLN.
|
| 180 |
+
Processes unified features with self-attention.
|
| 181 |
+
|
| 182 |
+
Improvements:
|
| 183 |
+
- Enhanced AdaLN control for unified stream
|
| 184 |
+
- Better style propagation through the network
|
| 185 |
+
- Optional sparse activation for MoE
|
| 186 |
+
"""
|
| 187 |
+
def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, use_sparse=False):
|
| 188 |
+
super().__init__(hidden_size, num_heads, mlp_ratio, use_sparse)
|
| 189 |
+
|
| 190 |
+
self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 191 |
+
self.qkv = nn.Linear(hidden_size, hidden_size * 3)
|
| 192 |
+
self.proj = nn.Linear(hidden_size, hidden_size)
|
| 193 |
+
self.qk_norm = QKNorm(self.head_dim)
|
| 194 |
+
|
| 195 |
+
self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 196 |
+
self.mlp = SwiGLU(hidden_size, int(hidden_size * mlp_ratio))
|
| 197 |
+
|
| 198 |
+
# AdaLN with 6 components (scale, shift, gate for attention and MLP)
|
| 199 |
+
self.adaLN = AdaLNModulation(hidden_size, components=6)
|
| 200 |
+
|
| 201 |
+
def forward(self, x, vec, pe):
|
| 202 |
+
"""
|
| 203 |
+
Forward pass with single-stream processing
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
x: Input features [B, L, D]
|
| 207 |
+
vec: Conditioning vector [B, D]
|
| 208 |
+
pe: Position embeddings
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
Updated features
|
| 212 |
+
"""
|
| 213 |
+
shift_a, scale_a, gate_a, shift_m, scale_m, gate_m = self.adaLN(vec)
|
| 214 |
+
|
| 215 |
+
# Attention with AdaLN
|
| 216 |
+
x_mod = self.apply_adaptive_norm(self.norm1(x), scale_a, shift_a)
|
| 217 |
+
qkv = self.qkv(x_mod)
|
| 218 |
+
q, k, v = rearrange(qkv, "B L (K H D) -> K B L H D", K=3, H=self.num_heads)
|
| 219 |
+
q, k = self.qk_norm(q, k)
|
| 220 |
+
|
| 221 |
+
dtype = x.dtype
|
| 222 |
+
q, k, v = q.to(dtype), k.to(dtype), v.to(dtype)
|
| 223 |
+
|
| 224 |
+
# Apply RoPE if position embeddings provided
|
| 225 |
+
if pe is not None:
|
| 226 |
+
pe_reshaped = pe.transpose(1, 2)
|
| 227 |
+
q, k = apply_rope(q, k, pe_reshaped)
|
| 228 |
+
q, k = q.to(dtype), k.to(dtype)
|
| 229 |
+
|
| 230 |
+
x_attn = self.proj(attention(q, k, v))
|
| 231 |
+
x = x + gate_a.unsqueeze(1) * x_attn
|
| 232 |
+
|
| 233 |
+
# MLP with AdaLN
|
| 234 |
+
x_mod = self.apply_adaptive_norm(self.norm2(x), scale_m, shift_m)
|
| 235 |
+
x = x + gate_m.unsqueeze(1) * self.mlp(x_mod)
|
| 236 |
+
|
| 237 |
+
return x
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
# For backward compatibility, create aliases
|
| 241 |
+
SwiftDoubleStreamBlock = AdaptiveDoubleStreamBlock
|
| 242 |
+
SwiftSingleStreamBlock = AdaptiveSingleStreamBlock
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
__all__ = [
|
| 246 |
+
'AdaptiveFluxBlock',
|
| 247 |
+
'AdaptiveDoubleStreamBlock',
|
| 248 |
+
'AdaptiveSingleStreamBlock',
|
| 249 |
+
'SwiftDoubleStreamBlock',
|
| 250 |
+
'SwiftSingleStreamBlock',
|
| 251 |
+
'QKNorm',
|
| 252 |
+
'SwiGLU',
|
| 253 |
+
]
|
flux/modules/advanced_encoders.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Advanced encoder modules for enhanced feature extraction.
|
| 3 |
+
Supports DINOv2, EVA-CLIP, and other state-of-the-art vision encoders.
|
| 4 |
+
Author: https://github.com/datnguyen-tien204
|
| 5 |
+
"""
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from transformers import AutoModel, AutoImageProcessor, AutoImageProcessor, AutoModelForDepthEstimation
|
| 10 |
+
from typing import Optional, Literal
|
| 11 |
+
from flux.utils.logger_utils import ConsoleLogger
|
| 12 |
+
|
| 13 |
+
class DepthEstimator:
|
| 14 |
+
def __init__(self, device):
|
| 15 |
+
self.device = device
|
| 16 |
+
ConsoleLogger.info("Loading Depth Anything V2 Small...")
|
| 17 |
+
self.proc = AutoImageProcessor.from_pretrained("depth-anything/Depth-Anything-V2-Small-hf")
|
| 18 |
+
self.model = AutoModelForDepthEstimation.from_pretrained("depth-anything/Depth-Anything-V2-Small-hf").to(device).eval()
|
| 19 |
+
|
| 20 |
+
@torch.no_grad()
|
| 21 |
+
def predict(self, images, size=384):
|
| 22 |
+
imgs_255 = ((images * 0.5 + 0.5) * 255).clamp(0, 255).to(torch.uint8)
|
| 23 |
+
inputs = self.proc(images=imgs_255, return_tensors="pt", size={"height": 518, "width": 518}).to(self.device)
|
| 24 |
+
depth = self.model(**inputs).predicted_depth
|
| 25 |
+
depth = F.interpolate(depth.unsqueeze(1), size=(size, size), mode="bicubic")
|
| 26 |
+
for i in range(depth.shape[0]):
|
| 27 |
+
depth[i] = (depth[i] - depth[i].min()) / (depth[i].max() - depth[i].min() + 1e-6)
|
| 28 |
+
return depth
|
| 29 |
+
|
| 30 |
+
class DINOv2Encoder(nn.Module):
|
| 31 |
+
"""
|
| 32 |
+
DINOv2 encoder for enhanced feature extraction.
|
| 33 |
+
Better semantic understanding and structural preservation.
|
| 34 |
+
"""
|
| 35 |
+
def __init__(self, device: str, output_dim: int = 512,
|
| 36 |
+
model_size: Literal["small", "base", "large"] = "base"):
|
| 37 |
+
super().__init__()
|
| 38 |
+
self.device = device
|
| 39 |
+
self.output_dim = output_dim
|
| 40 |
+
|
| 41 |
+
# Select model based on size
|
| 42 |
+
model_names = {
|
| 43 |
+
"small": "facebook/dinov2-small",
|
| 44 |
+
"base": "facebook/dinov2-base",
|
| 45 |
+
"large": "facebook/dinov2-large"
|
| 46 |
+
}
|
| 47 |
+
model_name = model_names[model_size]
|
| 48 |
+
|
| 49 |
+
print(f"Loading DINOv2 encoder: {model_name}")
|
| 50 |
+
self.model = AutoModel.from_pretrained(model_name).to(device)
|
| 51 |
+
self.model.eval()
|
| 52 |
+
|
| 53 |
+
# Get hidden size from model config
|
| 54 |
+
hidden_size = self.model.config.hidden_size
|
| 55 |
+
|
| 56 |
+
# Projection layer to match output_dim
|
| 57 |
+
self.proj = nn.Linear(hidden_size, output_dim, bias=False).to(device)
|
| 58 |
+
|
| 59 |
+
def forward(self, x: torch.Tensor, return_spatial: bool = False) -> torch.Tensor:
|
| 60 |
+
"""
|
| 61 |
+
Args:
|
| 62 |
+
x: Input images [B, 3, H, W] in range [-1, 1]
|
| 63 |
+
return_spatial: Whether to return all patch tokens [B, L, D]
|
| 64 |
+
Returns:
|
| 65 |
+
features: [B, output_dim] or [B, L, output_dim]
|
| 66 |
+
"""
|
| 67 |
+
# Normalize to [0, 1] for DINOv2
|
| 68 |
+
x_normalized = (x + 1.0) / 2.0
|
| 69 |
+
|
| 70 |
+
# Resize to 224x224 (DINOv2 standard input size)
|
| 71 |
+
x_resized = F.interpolate(x_normalized, size=(224, 224),
|
| 72 |
+
mode='bilinear', align_corners=False)
|
| 73 |
+
|
| 74 |
+
with torch.no_grad():
|
| 75 |
+
outputs = self.model(pixel_values=x_resized, return_dict=True)
|
| 76 |
+
if return_spatial:
|
| 77 |
+
# Return all patch tokens (excluding CLS if possible, though Flux handle sequences well)
|
| 78 |
+
features = outputs.last_hidden_state
|
| 79 |
+
else:
|
| 80 |
+
# Use CLS token (first token) as global feature
|
| 81 |
+
features = outputs.last_hidden_state[:, 0]
|
| 82 |
+
|
| 83 |
+
return self.proj(features)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class EVACLIPEncoder(nn.Module):
|
| 87 |
+
"""
|
| 88 |
+
EVA-CLIP encoder for enhanced vision-language feature extraction.
|
| 89 |
+
State-of-the-art performance on visual understanding tasks.
|
| 90 |
+
"""
|
| 91 |
+
def __init__(self, device: str, output_dim: int = 512,
|
| 92 |
+
model_size: Literal["base", "large"] = "large"):
|
| 93 |
+
super().__init__()
|
| 94 |
+
self.device = device
|
| 95 |
+
self.output_dim = output_dim
|
| 96 |
+
|
| 97 |
+
# EVA-CLIP models
|
| 98 |
+
model_names = {
|
| 99 |
+
"base": "BAAI/EVA-CLIP-8B",
|
| 100 |
+
"large": "BAAI/EVA-CLIP-18B"
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Note: Using a more accessible model for now
|
| 104 |
+
# In production, use actual EVA-CLIP if available
|
| 105 |
+
model_name = "laion/CLIP-ViT-H-14-laion2B-s32B-b79K"
|
| 106 |
+
|
| 107 |
+
print(f"Loading EVA-CLIP style encoder: {model_name}")
|
| 108 |
+
try:
|
| 109 |
+
from transformers import CLIPVisionModel
|
| 110 |
+
self.model = CLIPVisionModel.from_pretrained(model_name).to(device)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
print(f"Warning: Could not load {model_name}, falling back to openai/clip-vit-large-patch14")
|
| 113 |
+
from transformers import CLIPVisionModel
|
| 114 |
+
self.model = CLIPVisionModel.from_pretrained("openai/clip-vit-large-patch14").to(device)
|
| 115 |
+
|
| 116 |
+
self.model.eval()
|
| 117 |
+
|
| 118 |
+
# Get hidden size
|
| 119 |
+
hidden_size = self.model.config.hidden_size
|
| 120 |
+
|
| 121 |
+
# Projection layer
|
| 122 |
+
self.proj = nn.Linear(hidden_size, output_dim, bias=False).to(device)
|
| 123 |
+
|
| 124 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 125 |
+
"""
|
| 126 |
+
Args:
|
| 127 |
+
x: Input images [B, 3, H, W] in range [-1, 1]
|
| 128 |
+
Returns:
|
| 129 |
+
features: [B, output_dim]
|
| 130 |
+
"""
|
| 131 |
+
# Normalize to [0, 1]
|
| 132 |
+
x_normalized = (x + 1.0) / 2.0
|
| 133 |
+
|
| 134 |
+
# Resize to 224x224
|
| 135 |
+
x_resized = F.interpolate(x_normalized, size=(224, 224),
|
| 136 |
+
mode='bilinear', align_corners=False)
|
| 137 |
+
|
| 138 |
+
with torch.no_grad():
|
| 139 |
+
outputs = self.model(pixel_values=x_resized, return_dict=True)
|
| 140 |
+
features = outputs.pooler_output
|
| 141 |
+
|
| 142 |
+
return self.proj(features)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class DepthAwareEncoder(nn.Module):
|
| 146 |
+
"""
|
| 147 |
+
Depth-aware feature encoder that combines visual features with depth information.
|
| 148 |
+
"""
|
| 149 |
+
def __init__(self, base_encoder: nn.Module, depth_channels: int = 1,
|
| 150 |
+
output_dim: int = 512):
|
| 151 |
+
super().__init__()
|
| 152 |
+
self.base_encoder = base_encoder
|
| 153 |
+
self.output_dim = output_dim
|
| 154 |
+
|
| 155 |
+
# Depth processing branch
|
| 156 |
+
self.depth_conv = nn.Sequential(
|
| 157 |
+
nn.Conv2d(depth_channels, 32, kernel_size=3, stride=2, padding=1),
|
| 158 |
+
nn.ReLU(inplace=True),
|
| 159 |
+
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),
|
| 160 |
+
nn.ReLU(inplace=True),
|
| 161 |
+
nn.AdaptiveAvgPool2d(1)
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# Fusion layer
|
| 165 |
+
self.fusion = nn.Sequential(
|
| 166 |
+
nn.Linear(output_dim + 64, output_dim),
|
| 167 |
+
nn.ReLU(inplace=True),
|
| 168 |
+
nn.Linear(output_dim, output_dim)
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def forward(self, x: torch.Tensor, depth: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 172 |
+
"""
|
| 173 |
+
Args:
|
| 174 |
+
x: Input images [B, 3, H, W]
|
| 175 |
+
depth: Depth maps [B, 1, H, W] (optional)
|
| 176 |
+
Returns:
|
| 177 |
+
features: [B, output_dim]
|
| 178 |
+
"""
|
| 179 |
+
# Get base visual features
|
| 180 |
+
visual_features = self.base_encoder(x)
|
| 181 |
+
|
| 182 |
+
if depth is None:
|
| 183 |
+
return visual_features
|
| 184 |
+
|
| 185 |
+
# Process depth
|
| 186 |
+
depth_features = self.depth_conv(depth)
|
| 187 |
+
depth_features = depth_features.view(depth_features.size(0), -1)
|
| 188 |
+
|
| 189 |
+
# Concatenate and fuse
|
| 190 |
+
combined = torch.cat([visual_features, depth_features], dim=1)
|
| 191 |
+
fused_features = self.fusion(combined)
|
| 192 |
+
|
| 193 |
+
return fused_features
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
class EncoderFactory:
|
| 197 |
+
"""
|
| 198 |
+
Factory class for creating different types of encoders.
|
| 199 |
+
"""
|
| 200 |
+
@staticmethod
|
| 201 |
+
def create_encoder(
|
| 202 |
+
encoder_type: Literal["siglip", "dinov2", "evaclip"],
|
| 203 |
+
device: str,
|
| 204 |
+
output_dim: int = 512,
|
| 205 |
+
depth_aware: bool = False,
|
| 206 |
+
**kwargs
|
| 207 |
+
) -> nn.Module:
|
| 208 |
+
"""
|
| 209 |
+
Create an encoder of the specified type.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
encoder_type: Type of encoder to create
|
| 213 |
+
device: Device to load the model on
|
| 214 |
+
output_dim: Output dimension of the encoder
|
| 215 |
+
depth_aware: Whether to wrap encoder with depth-aware processing
|
| 216 |
+
**kwargs: Additional arguments for specific encoders
|
| 217 |
+
|
| 218 |
+
Returns:
|
| 219 |
+
encoder: Configured encoder module
|
| 220 |
+
"""
|
| 221 |
+
if encoder_type == "siglip":
|
| 222 |
+
from transformers import SiglipVisionModel
|
| 223 |
+
|
| 224 |
+
class SigLIPStyleEncoder(nn.Module):
|
| 225 |
+
def __init__(self, device, output_dim=512):
|
| 226 |
+
super().__init__()
|
| 227 |
+
model_name = "google/siglip-so400m-patch14-384"
|
| 228 |
+
self.model = SiglipVisionModel.from_pretrained(model_name).to(device)
|
| 229 |
+
self.model.eval()
|
| 230 |
+
self.proj = nn.Linear(self.model.config.hidden_size, output_dim, bias=False).to(device)
|
| 231 |
+
|
| 232 |
+
def forward(self, x):
|
| 233 |
+
x_resized = F.interpolate(x, size=(384, 384), mode='bilinear', align_corners=False)
|
| 234 |
+
with torch.no_grad():
|
| 235 |
+
outputs = self.model(pixel_values=x_resized)
|
| 236 |
+
features = outputs.last_hidden_state.mean(dim=1)
|
| 237 |
+
return self.proj(features)
|
| 238 |
+
|
| 239 |
+
encoder = SigLIPStyleEncoder(device, output_dim)
|
| 240 |
+
|
| 241 |
+
elif encoder_type == "dinov2":
|
| 242 |
+
model_size = kwargs.get("model_size", "base")
|
| 243 |
+
encoder = DINOv2Encoder(device, output_dim, model_size)
|
| 244 |
+
|
| 245 |
+
elif encoder_type == "evaclip":
|
| 246 |
+
model_size = kwargs.get("model_size", "large")
|
| 247 |
+
encoder = EVACLIPEncoder(device, output_dim, model_size)
|
| 248 |
+
|
| 249 |
+
else:
|
| 250 |
+
raise ValueError(f"Unknown encoder type: {encoder_type}")
|
| 251 |
+
|
| 252 |
+
# Wrap with depth-aware processing if requested
|
| 253 |
+
if depth_aware:
|
| 254 |
+
encoder = DepthAwareEncoder(encoder, depth_channels=1, output_dim=output_dim)
|
| 255 |
+
|
| 256 |
+
return encoder
|
flux/modules/attention.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Advanced attention mechanisms for better semantic and style alignment.
|
| 3 |
+
Includes Cross-Attention, AdaIN, and Depth-Controlled Attention.
|
| 4 |
+
Author: https://github.com/datnguyen-tien204
|
| 5 |
+
"""
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from einops import rearrange
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AdaptiveInstanceNorm2d(nn.Module):
|
| 14 |
+
"""
|
| 15 |
+
Adaptive Instance Normalization (AdaIN) for style transfer.
|
| 16 |
+
Aligns mean and variance of content features to match style features.
|
| 17 |
+
"""
|
| 18 |
+
def __init__(self, num_features: int, style_dim: int):
|
| 19 |
+
"""
|
| 20 |
+
Args:
|
| 21 |
+
num_features: Number of feature channels
|
| 22 |
+
style_dim: Dimension of style vector
|
| 23 |
+
"""
|
| 24 |
+
super().__init__()
|
| 25 |
+
self.num_features = num_features
|
| 26 |
+
|
| 27 |
+
# Style modulation parameters
|
| 28 |
+
self.style_scale = nn.Linear(style_dim, num_features)
|
| 29 |
+
self.style_shift = nn.Linear(style_dim, num_features)
|
| 30 |
+
|
| 31 |
+
# Initialize to identity transformation
|
| 32 |
+
nn.init.constant_(self.style_scale.weight, 0)
|
| 33 |
+
nn.init.constant_(self.style_scale.bias, 1)
|
| 34 |
+
nn.init.constant_(self.style_shift.weight, 0)
|
| 35 |
+
nn.init.constant_(self.style_shift.bias, 0)
|
| 36 |
+
|
| 37 |
+
def forward(self, x: torch.Tensor, style: torch.Tensor) -> torch.Tensor:
|
| 38 |
+
"""
|
| 39 |
+
Args:
|
| 40 |
+
x: Input features [B, C, H, W]
|
| 41 |
+
style: Style vector [B, style_dim]
|
| 42 |
+
Returns:
|
| 43 |
+
normalized: Style-adapted features [B, C, H, W]
|
| 44 |
+
"""
|
| 45 |
+
# Instance normalization
|
| 46 |
+
mean = x.mean(dim=[2, 3], keepdim=True)
|
| 47 |
+
std = x.std(dim=[2, 3], keepdim=True) + 1e-5
|
| 48 |
+
normalized = (x - mean) / std
|
| 49 |
+
|
| 50 |
+
# Style modulation
|
| 51 |
+
scale = self.style_scale(style).unsqueeze(-1).unsqueeze(-1)
|
| 52 |
+
shift = self.style_shift(style).unsqueeze(-1).unsqueeze(-1)
|
| 53 |
+
|
| 54 |
+
return normalized * scale + shift
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class AdaIN1d(nn.Module):
|
| 58 |
+
"""
|
| 59 |
+
AdaIN for 1D features (sequence data).
|
| 60 |
+
"""
|
| 61 |
+
def __init__(self, num_features: int, style_dim: int):
|
| 62 |
+
super().__init__()
|
| 63 |
+
self.num_features = num_features
|
| 64 |
+
|
| 65 |
+
self.norm = nn.LayerNorm(num_features, elementwise_affine=False)
|
| 66 |
+
self.style_scale = nn.Linear(style_dim, num_features)
|
| 67 |
+
self.style_shift = nn.Linear(style_dim, num_features)
|
| 68 |
+
|
| 69 |
+
nn.init.constant_(self.style_scale.weight, 0)
|
| 70 |
+
nn.init.constant_(self.style_scale.bias, 1)
|
| 71 |
+
nn.init.constant_(self.style_shift.weight, 0)
|
| 72 |
+
nn.init.constant_(self.style_shift.bias, 0)
|
| 73 |
+
|
| 74 |
+
def forward(self, x: torch.Tensor, style: torch.Tensor) -> torch.Tensor:
|
| 75 |
+
"""
|
| 76 |
+
Args:
|
| 77 |
+
x: Input features [B, L, C]
|
| 78 |
+
style: Style vector [B, style_dim] OR [B, L_s, style_dim]
|
| 79 |
+
Returns:
|
| 80 |
+
normalized: Style-adapted features [B, L, C]
|
| 81 |
+
"""
|
| 82 |
+
if style.dim() == 3:
|
| 83 |
+
style = style.mean(dim=1)
|
| 84 |
+
|
| 85 |
+
normalized = self.norm(x)
|
| 86 |
+
scale = self.style_scale(style).unsqueeze(1)
|
| 87 |
+
shift = self.style_shift(style).unsqueeze(1)
|
| 88 |
+
|
| 89 |
+
return normalized * scale + shift
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class CrossAttention(nn.Module):
|
| 94 |
+
"""
|
| 95 |
+
Cross-Attention mechanism for semantic alignment between content and style.
|
| 96 |
+
"""
|
| 97 |
+
def __init__(self, query_dim: int, key_dim: int, num_heads: int = 8,
|
| 98 |
+
dropout: float = 0.0):
|
| 99 |
+
"""
|
| 100 |
+
Args:
|
| 101 |
+
query_dim: Dimension of query features
|
| 102 |
+
key_dim: Dimension of key/value features
|
| 103 |
+
num_heads: Number of attention heads
|
| 104 |
+
dropout: Dropout probability
|
| 105 |
+
"""
|
| 106 |
+
super().__init__()
|
| 107 |
+
self.num_heads = num_heads
|
| 108 |
+
self.head_dim = query_dim // num_heads
|
| 109 |
+
assert query_dim % num_heads == 0, "query_dim must be divisible by num_heads"
|
| 110 |
+
|
| 111 |
+
self.query = nn.Linear(query_dim, query_dim)
|
| 112 |
+
self.key = nn.Linear(key_dim, query_dim)
|
| 113 |
+
self.value = nn.Linear(key_dim, query_dim)
|
| 114 |
+
|
| 115 |
+
self.proj = nn.Linear(query_dim, query_dim)
|
| 116 |
+
self.dropout = nn.Dropout(dropout)
|
| 117 |
+
|
| 118 |
+
def forward(self, query: torch.Tensor, key: torch.Tensor,
|
| 119 |
+
value: Optional[torch.Tensor] = None,
|
| 120 |
+
mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 121 |
+
"""
|
| 122 |
+
Args:
|
| 123 |
+
query: Query features [B, L_q, query_dim]
|
| 124 |
+
key: Key features [B, L_k, key_dim]
|
| 125 |
+
value: Value features [B, L_k, key_dim] (if None, uses key)
|
| 126 |
+
mask: Attention mask [B, L_q, L_k] (optional)
|
| 127 |
+
Returns:
|
| 128 |
+
output: Attended features [B, L_q, query_dim]
|
| 129 |
+
"""
|
| 130 |
+
if value is None:
|
| 131 |
+
value = key
|
| 132 |
+
|
| 133 |
+
B, L_q, _ = query.shape
|
| 134 |
+
L_k = key.shape[1]
|
| 135 |
+
|
| 136 |
+
# Linear projections
|
| 137 |
+
q = self.query(query)
|
| 138 |
+
k = self.key(key)
|
| 139 |
+
v = self.value(value)
|
| 140 |
+
|
| 141 |
+
# Reshape for multi-head attention
|
| 142 |
+
q = rearrange(q, 'b l (h d) -> b h l d', h=self.num_heads)
|
| 143 |
+
k = rearrange(k, 'b l (h d) -> b h l d', h=self.num_heads)
|
| 144 |
+
v = rearrange(v, 'b l (h d) -> b h l d', h=self.num_heads)
|
| 145 |
+
|
| 146 |
+
# Scaled dot-product attention
|
| 147 |
+
scale = self.head_dim ** -0.5
|
| 148 |
+
attn = torch.matmul(q, k.transpose(-2, -1)) * scale
|
| 149 |
+
|
| 150 |
+
if mask is not None:
|
| 151 |
+
attn = attn.masked_fill(mask.unsqueeze(1) == 0, float('-inf'))
|
| 152 |
+
|
| 153 |
+
attn = F.softmax(attn, dim=-1)
|
| 154 |
+
attn = self.dropout(attn)
|
| 155 |
+
|
| 156 |
+
# Apply attention to values
|
| 157 |
+
out = torch.matmul(attn, v)
|
| 158 |
+
out = rearrange(out, 'b h l d -> b l (h d)')
|
| 159 |
+
|
| 160 |
+
# Output projection
|
| 161 |
+
out = self.proj(out)
|
| 162 |
+
|
| 163 |
+
return out
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class DepthControlledAttention(nn.Module):
|
| 167 |
+
"""
|
| 168 |
+
Depth-controlled attention mechanism for structure-aware feature learning.
|
| 169 |
+
Uses depth information to modulate attention patterns.
|
| 170 |
+
"""
|
| 171 |
+
def __init__(self, feature_dim: int, depth_dim: int = 64, num_heads: int = 8):
|
| 172 |
+
"""
|
| 173 |
+
Args:
|
| 174 |
+
feature_dim: Dimension of input features
|
| 175 |
+
depth_dim: Dimension of depth features
|
| 176 |
+
num_heads: Number of attention heads
|
| 177 |
+
"""
|
| 178 |
+
super().__init__()
|
| 179 |
+
self.num_heads = num_heads
|
| 180 |
+
self.head_dim = feature_dim // num_heads
|
| 181 |
+
|
| 182 |
+
# Process depth information
|
| 183 |
+
self.depth_encoder = nn.Sequential(
|
| 184 |
+
nn.Conv2d(1, depth_dim, kernel_size=3, padding=1),
|
| 185 |
+
nn.ReLU(inplace=True),
|
| 186 |
+
nn.Conv2d(depth_dim, depth_dim, kernel_size=3, padding=1)
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Depth-modulated attention
|
| 190 |
+
self.qkv = nn.Linear(feature_dim, feature_dim * 3)
|
| 191 |
+
self.depth_gate = nn.Linear(depth_dim, feature_dim)
|
| 192 |
+
self.proj = nn.Linear(feature_dim, feature_dim)
|
| 193 |
+
|
| 194 |
+
def forward(self, x: torch.Tensor, depth: torch.Tensor) -> torch.Tensor:
|
| 195 |
+
"""
|
| 196 |
+
Args:
|
| 197 |
+
x: Input features [B, L, C]
|
| 198 |
+
depth: Depth map [B, 1, H, W]
|
| 199 |
+
Returns:
|
| 200 |
+
output: Depth-modulated features [B, L, C]
|
| 201 |
+
"""
|
| 202 |
+
B, L, C = x.shape
|
| 203 |
+
|
| 204 |
+
# Encode depth
|
| 205 |
+
depth_feat = self.depth_encoder(depth) # [B, depth_dim, H, W]
|
| 206 |
+
H, W = depth_feat.shape[-2:]
|
| 207 |
+
depth_feat = rearrange(depth_feat, 'b c h w -> b (h w) c')
|
| 208 |
+
|
| 209 |
+
# Generate Q, K, V
|
| 210 |
+
qkv = self.qkv(x)
|
| 211 |
+
q, k, v = rearrange(qkv, 'b l (three h d) -> three b h l d',
|
| 212 |
+
three=3, h=self.num_heads)
|
| 213 |
+
|
| 214 |
+
# Compute attention
|
| 215 |
+
scale = self.head_dim ** -0.5
|
| 216 |
+
attn = torch.matmul(q, k.transpose(-2, -1)) * scale
|
| 217 |
+
|
| 218 |
+
# Modulate attention with depth
|
| 219 |
+
if depth_feat.shape[1] == L:
|
| 220 |
+
depth_gate = self.depth_gate(depth_feat)
|
| 221 |
+
depth_gate = rearrange(depth_gate, 'b l (h d) -> b h l d', h=self.num_heads)
|
| 222 |
+
depth_mask = torch.sigmoid(depth_gate.mean(dim=-1, keepdim=True))
|
| 223 |
+
attn = attn * depth_mask
|
| 224 |
+
|
| 225 |
+
attn = F.softmax(attn, dim=-1)
|
| 226 |
+
|
| 227 |
+
# Apply attention
|
| 228 |
+
out = torch.matmul(attn, v)
|
| 229 |
+
out = rearrange(out, 'b h l d -> b l (h d)')
|
| 230 |
+
out = self.proj(out)
|
| 231 |
+
|
| 232 |
+
return out
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class StyleAttentionBlock(nn.Module):
|
| 236 |
+
"""
|
| 237 |
+
Combined attention block with AdaIN and Cross-Attention.
|
| 238 |
+
"""
|
| 239 |
+
def __init__(self, feature_dim: int, style_dim: int, num_heads: int = 8):
|
| 240 |
+
super().__init__()
|
| 241 |
+
|
| 242 |
+
# Layer normalization
|
| 243 |
+
self.norm1 = nn.LayerNorm(feature_dim)
|
| 244 |
+
self.norm2 = nn.LayerNorm(feature_dim)
|
| 245 |
+
|
| 246 |
+
# Cross-attention for style conditioning
|
| 247 |
+
self.cross_attn = CrossAttention(feature_dim, style_dim, num_heads)
|
| 248 |
+
|
| 249 |
+
# AdaIN for style transfer
|
| 250 |
+
self.adain = AdaIN1d(feature_dim, style_dim)
|
| 251 |
+
|
| 252 |
+
# Feed-forward network
|
| 253 |
+
self.ffn = nn.Sequential(
|
| 254 |
+
nn.Linear(feature_dim, feature_dim * 4),
|
| 255 |
+
nn.GELU(),
|
| 256 |
+
nn.Linear(feature_dim * 4, feature_dim)
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def forward(self, x: torch.Tensor, style: torch.Tensor,
|
| 260 |
+
style_features: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 261 |
+
"""
|
| 262 |
+
Args:
|
| 263 |
+
x: Input features [B, L, C]
|
| 264 |
+
style: Style vector [B, style_dim]
|
| 265 |
+
style_features: Optional style feature sequence [B, L_s, style_dim]
|
| 266 |
+
Returns:
|
| 267 |
+
output: Style-conditioned features [B, L, C]
|
| 268 |
+
"""
|
| 269 |
+
# Cross-attention with style features
|
| 270 |
+
if style_features is not None:
|
| 271 |
+
attn_out = self.cross_attn(self.norm1(x), style_features)
|
| 272 |
+
x = x + attn_out
|
| 273 |
+
|
| 274 |
+
# AdaIN style transfer
|
| 275 |
+
x = self.adain(x, style)
|
| 276 |
+
|
| 277 |
+
# Feed-forward
|
| 278 |
+
x = x + self.ffn(self.norm2(x))
|
| 279 |
+
|
| 280 |
+
return x
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class MultiScaleAttention(nn.Module):
|
| 284 |
+
"""
|
| 285 |
+
Multi-scale attention for capturing features at different resolutions.
|
| 286 |
+
"""
|
| 287 |
+
def __init__(self, feature_dim: int, num_scales: int = 3, num_heads: int = 8):
|
| 288 |
+
super().__init__()
|
| 289 |
+
self.num_scales = num_scales
|
| 290 |
+
|
| 291 |
+
self.attentions = nn.ModuleList([
|
| 292 |
+
nn.MultiheadAttention(feature_dim, num_heads, batch_first=True)
|
| 293 |
+
for _ in range(num_scales)
|
| 294 |
+
])
|
| 295 |
+
|
| 296 |
+
self.fusion = nn.Linear(feature_dim * num_scales, feature_dim)
|
| 297 |
+
|
| 298 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 299 |
+
"""
|
| 300 |
+
Args:
|
| 301 |
+
x: Input features [B, L, C]
|
| 302 |
+
Returns:
|
| 303 |
+
output: Multi-scale attended features [B, L, C]
|
| 304 |
+
"""
|
| 305 |
+
B, L, C = x.shape
|
| 306 |
+
|
| 307 |
+
outputs = []
|
| 308 |
+
for i, attn in enumerate(self.attentions):
|
| 309 |
+
# Downsample for different scales
|
| 310 |
+
if i > 0:
|
| 311 |
+
scale = 2 ** i
|
| 312 |
+
# Simple downsampling by reshaping
|
| 313 |
+
L_down = L // scale
|
| 314 |
+
if L_down > 0:
|
| 315 |
+
x_down = x[:, ::scale, :]
|
| 316 |
+
else:
|
| 317 |
+
x_down = x
|
| 318 |
+
out, _ = attn(x_down, x_down, x_down)
|
| 319 |
+
# Upsample back
|
| 320 |
+
if out.shape[1] != L:
|
| 321 |
+
out = F.interpolate(out.transpose(1, 2), size=L, mode='linear',
|
| 322 |
+
align_corners=False).transpose(1, 2)
|
| 323 |
+
else:
|
| 324 |
+
out, _ = attn(x, x, x)
|
| 325 |
+
|
| 326 |
+
outputs.append(out)
|
| 327 |
+
|
| 328 |
+
# Concatenate and fuse
|
| 329 |
+
multi_scale = torch.cat(outputs, dim=-1)
|
| 330 |
+
fused = self.fusion(multi_scale)
|
| 331 |
+
|
| 332 |
+
return fused
|
flux/modules/autoencoder.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flux project: https://github.com/black-forest-labs/flux/blob/main/src/flux/modules/autoencoder.py
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
import torch
|
| 4 |
+
from einops import rearrange
|
| 5 |
+
from torch import Tensor, nn
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class AutoEncoderParams:
|
| 9 |
+
resolution: int
|
| 10 |
+
in_channels: int
|
| 11 |
+
ch: int
|
| 12 |
+
out_ch: int
|
| 13 |
+
ch_mult: list[int]
|
| 14 |
+
num_res_blocks: int
|
| 15 |
+
z_channels: int
|
| 16 |
+
scale_factor: float
|
| 17 |
+
shift_factor: float
|
| 18 |
+
|
| 19 |
+
def swish(x: Tensor) -> Tensor:
|
| 20 |
+
return x * torch.sigmoid(x)
|
| 21 |
+
|
| 22 |
+
class AttnBlock(nn.Module):
|
| 23 |
+
def __init__(self, in_channels: int):
|
| 24 |
+
super().__init__()
|
| 25 |
+
self.in_channels = in_channels
|
| 26 |
+
self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
|
| 27 |
+
self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 28 |
+
self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 29 |
+
self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 30 |
+
self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 31 |
+
|
| 32 |
+
def attention(self, h_: Tensor) -> Tensor:
|
| 33 |
+
h_ = self.norm(h_)
|
| 34 |
+
q = self.q(h_)
|
| 35 |
+
k = self.k(h_)
|
| 36 |
+
v = self.v(h_)
|
| 37 |
+
b, c, h, w = q.shape
|
| 38 |
+
q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous()
|
| 39 |
+
k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous()
|
| 40 |
+
v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous()
|
| 41 |
+
h_ = nn.functional.scaled_dot_product_attention(q, k, v)
|
| 42 |
+
return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
|
| 43 |
+
|
| 44 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 45 |
+
return x + self.proj_out(self.attention(x))
|
| 46 |
+
|
| 47 |
+
class ResnetBlock(nn.Module):
|
| 48 |
+
def __init__(self, in_channels: int, out_channels: int):
|
| 49 |
+
super().__init__()
|
| 50 |
+
self.in_channels = in_channels
|
| 51 |
+
out_channels = in_channels if out_channels is None else out_channels
|
| 52 |
+
self.out_channels = out_channels
|
| 53 |
+
self.norm1 = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
|
| 54 |
+
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
|
| 55 |
+
self.norm2 = nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=1e-6, affine=True)
|
| 56 |
+
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
|
| 57 |
+
if self.in_channels != self.out_channels:
|
| 58 |
+
self.nin_shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
|
| 59 |
+
|
| 60 |
+
def forward(self, x):
|
| 61 |
+
h = x
|
| 62 |
+
h = self.norm1(h)
|
| 63 |
+
h = swish(h)
|
| 64 |
+
h = self.conv1(h)
|
| 65 |
+
h = self.norm2(h)
|
| 66 |
+
h = swish(h)
|
| 67 |
+
h = self.conv2(h)
|
| 68 |
+
if self.in_channels != self.out_channels:
|
| 69 |
+
x = self.nin_shortcut(x)
|
| 70 |
+
return x + h
|
| 71 |
+
|
| 72 |
+
class Downsample(nn.Module):
|
| 73 |
+
def __init__(self, in_channels: int):
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0)
|
| 76 |
+
|
| 77 |
+
def forward(self, x: Tensor):
|
| 78 |
+
pad = (0, 1, 0, 1)
|
| 79 |
+
x = nn.functional.pad(x, pad, mode="constant", value=0)
|
| 80 |
+
x = self.conv(x)
|
| 81 |
+
return x
|
| 82 |
+
|
| 83 |
+
class Upsample(nn.Module):
|
| 84 |
+
def __init__(self, in_channels: int):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
|
| 87 |
+
|
| 88 |
+
def forward(self, x: Tensor):
|
| 89 |
+
x = nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
|
| 90 |
+
x = self.conv(x)
|
| 91 |
+
return x
|
| 92 |
+
|
| 93 |
+
class Encoder(nn.Module):
|
| 94 |
+
def __init__(self, resolution: int, in_channels: int, ch: int, ch_mult: list[int], num_res_blocks: int, z_channels: int):
|
| 95 |
+
super().__init__()
|
| 96 |
+
self.ch = ch
|
| 97 |
+
self.num_resolutions = len(ch_mult)
|
| 98 |
+
self.num_res_blocks = num_res_blocks
|
| 99 |
+
self.resolution = resolution
|
| 100 |
+
self.in_channels = in_channels
|
| 101 |
+
self.conv_in = nn.Conv2d(in_channels, self.ch, kernel_size=3, stride=1, padding=1)
|
| 102 |
+
curr_res = resolution
|
| 103 |
+
in_ch_mult = (1,) + tuple(ch_mult)
|
| 104 |
+
self.in_ch_mult = in_ch_mult
|
| 105 |
+
self.down = nn.ModuleList()
|
| 106 |
+
block_in = self.ch
|
| 107 |
+
for i_level in range(self.num_resolutions):
|
| 108 |
+
block = nn.ModuleList()
|
| 109 |
+
attn = nn.ModuleList()
|
| 110 |
+
block_in = ch * in_ch_mult[i_level]
|
| 111 |
+
block_out = ch * ch_mult[i_level]
|
| 112 |
+
for _ in range(self.num_res_blocks):
|
| 113 |
+
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
|
| 114 |
+
block_in = block_out
|
| 115 |
+
down = nn.Module()
|
| 116 |
+
down.block = block
|
| 117 |
+
down.attn = attn
|
| 118 |
+
if i_level != self.num_resolutions - 1:
|
| 119 |
+
down.downsample = Downsample(block_in)
|
| 120 |
+
curr_res = curr_res // 2
|
| 121 |
+
self.down.append(down)
|
| 122 |
+
self.mid = nn.Module()
|
| 123 |
+
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 124 |
+
self.mid.attn_1 = AttnBlock(block_in)
|
| 125 |
+
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 126 |
+
self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
|
| 127 |
+
self.conv_out = nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1)
|
| 128 |
+
|
| 129 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 130 |
+
hs = [self.conv_in(x)]
|
| 131 |
+
for i_level in range(self.num_resolutions):
|
| 132 |
+
for i_block in range(self.num_res_blocks):
|
| 133 |
+
h = self.down[i_level].block[i_block](hs[-1])
|
| 134 |
+
if len(self.down[i_level].attn) > 0:
|
| 135 |
+
h = self.down[i_level].attn[i_block](h)
|
| 136 |
+
hs.append(h)
|
| 137 |
+
if i_level != self.num_resolutions - 1:
|
| 138 |
+
hs.append(self.down[i_level].downsample(hs[-1]))
|
| 139 |
+
h = hs[-1]
|
| 140 |
+
h = self.mid.block_1(h)
|
| 141 |
+
h = self.mid.attn_1(h)
|
| 142 |
+
h = self.mid.block_2(h)
|
| 143 |
+
h = self.norm_out(h)
|
| 144 |
+
h = swish(h)
|
| 145 |
+
h = self.conv_out(h)
|
| 146 |
+
return h
|
| 147 |
+
|
| 148 |
+
class Decoder(nn.Module):
|
| 149 |
+
def __init__(self, ch: int, out_ch: int, ch_mult: list[int], num_res_blocks: int, in_channels: int, resolution: int, z_channels: int):
|
| 150 |
+
super().__init__()
|
| 151 |
+
self.ch = ch
|
| 152 |
+
self.num_resolutions = len(ch_mult)
|
| 153 |
+
self.num_res_blocks = num_res_blocks
|
| 154 |
+
self.resolution = resolution
|
| 155 |
+
self.in_channels = in_channels
|
| 156 |
+
self.ffactor = 2 ** (self.num_resolutions - 1)
|
| 157 |
+
block_in = ch * ch_mult[self.num_resolutions - 1]
|
| 158 |
+
curr_res = resolution // 2 ** (self.num_resolutions - 1)
|
| 159 |
+
self.z_shape = (1, z_channels, curr_res, curr_res)
|
| 160 |
+
self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1)
|
| 161 |
+
self.mid = nn.Module()
|
| 162 |
+
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 163 |
+
self.mid.attn_1 = AttnBlock(block_in)
|
| 164 |
+
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 165 |
+
self.up = nn.ModuleList()
|
| 166 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 167 |
+
block = nn.ModuleList()
|
| 168 |
+
attn = nn.ModuleList()
|
| 169 |
+
block_out = ch * ch_mult[i_level]
|
| 170 |
+
for _ in range(self.num_res_blocks + 1):
|
| 171 |
+
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
|
| 172 |
+
block_in = block_out
|
| 173 |
+
up = nn.Module()
|
| 174 |
+
up.block = block
|
| 175 |
+
up.attn = attn
|
| 176 |
+
if i_level != 0:
|
| 177 |
+
up.upsample = Upsample(block_in)
|
| 178 |
+
curr_res = curr_res * 2
|
| 179 |
+
self.up.insert(0, up)
|
| 180 |
+
self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
|
| 181 |
+
self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
|
| 182 |
+
|
| 183 |
+
def forward(self, z: Tensor) -> Tensor:
|
| 184 |
+
upscale_dtype = next(self.up.parameters()).dtype
|
| 185 |
+
h = self.conv_in(z)
|
| 186 |
+
h = self.mid.block_1(h)
|
| 187 |
+
h = self.mid.attn_1(h)
|
| 188 |
+
h = self.mid.block_2(h)
|
| 189 |
+
h = h.to(upscale_dtype)
|
| 190 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 191 |
+
for i_block in range(self.num_res_blocks + 1):
|
| 192 |
+
h = self.up[i_level].block[i_block](h)
|
| 193 |
+
if len(self.up[i_level].attn) > 0:
|
| 194 |
+
h = self.up[i_level].attn[i_block](h)
|
| 195 |
+
if i_level != 0:
|
| 196 |
+
h = self.up[i_level].upsample(h)
|
| 197 |
+
h = self.norm_out(h)
|
| 198 |
+
h = swish(h)
|
| 199 |
+
h = self.conv_out(h)
|
| 200 |
+
return h
|
| 201 |
+
|
| 202 |
+
class DiagonalGaussian(nn.Module):
|
| 203 |
+
def __init__(self, sample: bool = True, chunk_dim: int = 1):
|
| 204 |
+
super().__init__()
|
| 205 |
+
self.sample = sample
|
| 206 |
+
self.chunk_dim = chunk_dim
|
| 207 |
+
|
| 208 |
+
def forward(self, z: Tensor) -> Tensor:
|
| 209 |
+
mean, logvar = torch.chunk(z, 2, dim=self.chunk_dim)
|
| 210 |
+
if self.sample:
|
| 211 |
+
std = torch.exp(0.5 * logvar)
|
| 212 |
+
return mean + std * torch.randn_like(mean)
|
| 213 |
+
else:
|
| 214 |
+
return mean
|
| 215 |
+
|
| 216 |
+
class AutoEncoder(nn.Module):
|
| 217 |
+
def __init__(self, params: AutoEncoderParams, sample_z: bool = False):
|
| 218 |
+
super().__init__()
|
| 219 |
+
self.params = params
|
| 220 |
+
self.encoder = Encoder(
|
| 221 |
+
resolution=params.resolution,
|
| 222 |
+
in_channels=params.in_channels,
|
| 223 |
+
ch=params.ch,
|
| 224 |
+
ch_mult=params.ch_mult,
|
| 225 |
+
num_res_blocks=params.num_res_blocks,
|
| 226 |
+
z_channels=params.z_channels,
|
| 227 |
+
)
|
| 228 |
+
self.decoder = Decoder(
|
| 229 |
+
resolution=params.resolution,
|
| 230 |
+
in_channels=params.in_channels,
|
| 231 |
+
ch=params.ch,
|
| 232 |
+
out_ch=params.out_ch,
|
| 233 |
+
ch_mult=params.ch_mult,
|
| 234 |
+
num_res_blocks=params.num_res_blocks,
|
| 235 |
+
z_channels=params.z_channels,
|
| 236 |
+
)
|
| 237 |
+
self.reg = DiagonalGaussian(sample=sample_z)
|
| 238 |
+
self.scale_factor = params.scale_factor
|
| 239 |
+
self.shift_factor = params.shift_factor
|
| 240 |
+
|
| 241 |
+
def encode(self, x: Tensor) -> Tensor:
|
| 242 |
+
z = self.reg(self.encoder(x))
|
| 243 |
+
z = self.scale_factor * (z - self.shift_factor)
|
| 244 |
+
return z
|
| 245 |
+
|
| 246 |
+
def decode(self, z: Tensor) -> Tensor:
|
| 247 |
+
z = z / self.scale_factor + self.shift_factor
|
| 248 |
+
return self.decoder(z)
|
| 249 |
+
|
| 250 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 251 |
+
return self.decode(self.encode(x))
|
flux/modules/conditioner.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flux Project: https://github.com/black-forest-labs/flux/blob/main/src/flux/modules/conditioner.py
|
| 2 |
+
from torch import Tensor, nn
|
| 3 |
+
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer
|
| 4 |
+
|
| 5 |
+
class HFEmbedder(nn.Module):
|
| 6 |
+
def __init__(self, version: str, max_length: int, **hf_kwargs):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.is_clip = version.startswith("openai")
|
| 9 |
+
self.max_length = max_length
|
| 10 |
+
self.output_key = "pooler_output" if self.is_clip else "last_hidden_state"
|
| 11 |
+
if self.is_clip:
|
| 12 |
+
self.tokenizer: CLIPTokenizer = CLIPTokenizer.from_pretrained(version, max_length=max_length)
|
| 13 |
+
self.hf_module: CLIPTextModel = CLIPTextModel.from_pretrained(version, **hf_kwargs)
|
| 14 |
+
else:
|
| 15 |
+
self.tokenizer: T5Tokenizer = T5Tokenizer.from_pretrained(version, max_length=max_length)
|
| 16 |
+
self.hf_module: T5EncoderModel = T5EncoderModel.from_pretrained(version, **hf_kwargs)
|
| 17 |
+
self.hf_module = self.hf_module.eval().requires_grad_(False)
|
| 18 |
+
|
| 19 |
+
def forward(self, text: list[str]) -> Tensor:
|
| 20 |
+
batch_encoding = self.tokenizer(
|
| 21 |
+
text,
|
| 22 |
+
truncation=True,
|
| 23 |
+
max_length=self.max_length,
|
| 24 |
+
return_length=False,
|
| 25 |
+
return_overflowing_tokens=False,
|
| 26 |
+
padding="max_length",
|
| 27 |
+
return_tensors="pt",
|
| 28 |
+
)
|
| 29 |
+
outputs = self.hf_module(
|
| 30 |
+
input_ids=batch_encoding["input_ids"].to(self.hf_module.device),
|
| 31 |
+
attention_mask=None,
|
| 32 |
+
output_hidden_states=False,
|
| 33 |
+
)
|
| 34 |
+
return outputs[self.output_key].bfloat16()
|
flux/modules/image_embedders.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flux Project: https://github.com/black-forest-labs/flux/blob/main/src/flux/modules/image_embedders.py
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from einops import rearrange, repeat
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from safetensors.torch import load_file as load_sft
|
| 8 |
+
from torch import nn
|
| 9 |
+
from transformers import AutoModelForDepthEstimation, AutoProcessor, SiglipImageProcessor, SiglipVisionModel
|
| 10 |
+
from flux.util import print_load_warning
|
| 11 |
+
|
| 12 |
+
class DepthImageEncoder:
|
| 13 |
+
depth_model_name = "LiheYoung/depth-anything-large-hf"
|
| 14 |
+
|
| 15 |
+
def __init__(self, device):
|
| 16 |
+
self.device = device
|
| 17 |
+
self.depth_model = AutoModelForDepthEstimation.from_pretrained(self.depth_model_name).to(device)
|
| 18 |
+
self.processor = AutoProcessor.from_pretrained(self.depth_model_name)
|
| 19 |
+
|
| 20 |
+
def __call__(self, img: torch.Tensor) -> torch.Tensor:
|
| 21 |
+
hw = img.shape[-2:]
|
| 22 |
+
img = torch.clamp(img, -1.0, 1.0)
|
| 23 |
+
img_byte = ((img + 1.0) * 127.5).byte()
|
| 24 |
+
img = self.processor(img_byte, return_tensors="pt")["pixel_values"]
|
| 25 |
+
depth = self.depth_model(img.to(self.device)).predicted_depth
|
| 26 |
+
depth = repeat(depth, "b h w -> b 3 h w")
|
| 27 |
+
depth = torch.nn.functional.interpolate(depth, hw, mode="bicubic", antialias=True)
|
| 28 |
+
depth = depth / 127.5 - 1.0
|
| 29 |
+
return depth
|
| 30 |
+
|
| 31 |
+
class CannyImageEncoder:
|
| 32 |
+
def __init__(self, device, min_t: int = 50, max_t: int = 200):
|
| 33 |
+
self.device = device
|
| 34 |
+
self.min_t = min_t
|
| 35 |
+
self.max_t = max_t
|
| 36 |
+
|
| 37 |
+
def __call__(self, img: torch.Tensor) -> torch.Tensor:
|
| 38 |
+
assert img.shape[0] == 1, "Only batch size 1 is supported"
|
| 39 |
+
img = rearrange(img[0], "c h w -> h w c")
|
| 40 |
+
img = torch.clamp(img, -1.0, 1.0)
|
| 41 |
+
img_np = ((img + 1.0) * 127.5).numpy().astype(np.uint8)
|
| 42 |
+
canny = cv2.Canny(img_np, self.min_t, self.max_t)
|
| 43 |
+
canny = torch.from_numpy(canny).float() / 127.5 - 1.0
|
| 44 |
+
canny = rearrange(canny, "h w -> 1 1 h w")
|
| 45 |
+
canny = repeat(canny, "b 1 ... -> b 3 ...")
|
| 46 |
+
return canny.to(self.device)
|
| 47 |
+
|
| 48 |
+
class ReduxImageEncoder(nn.Module):
|
| 49 |
+
siglip_model_name = "google/siglip-so400m-patch14-384"
|
| 50 |
+
|
| 51 |
+
def __init__(self, device, redux_path: str, redux_dim: int = 1152, txt_in_features: int = 4096, dtype=torch.bfloat16):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.redux_dim = redux_dim
|
| 54 |
+
self.device = device if isinstance(device, torch.device) else torch.device(device)
|
| 55 |
+
self.dtype = dtype
|
| 56 |
+
with self.device:
|
| 57 |
+
self.redux_up = nn.Linear(redux_dim, txt_in_features * 3, dtype=dtype)
|
| 58 |
+
self.redux_down = nn.Linear(txt_in_features * 3, txt_in_features, dtype=dtype)
|
| 59 |
+
sd = load_sft(redux_path, device=str(device))
|
| 60 |
+
missing, unexpected = self.load_state_dict(sd, strict=False, assign=True)
|
| 61 |
+
print_load_warning(missing, unexpected)
|
| 62 |
+
self.siglip = SiglipVisionModel.from_pretrained(self.siglip_model_name).to(dtype=dtype)
|
| 63 |
+
self.normalize = SiglipImageProcessor.from_pretrained(self.siglip_model_name)
|
| 64 |
+
|
| 65 |
+
def __call__(self, x: Image.Image) -> torch.Tensor:
|
| 66 |
+
imgs = self.normalize.preprocess(images=[x], do_resize=True, return_tensors="pt", do_convert_rgb=True)
|
| 67 |
+
_encoded_x = self.siglip(**imgs.to(device=self.device, dtype=self.dtype)).last_hidden_state
|
| 68 |
+
projected_x = self.redux_down(nn.functional.silu(self.redux_up(_encoded_x)))
|
| 69 |
+
return projected_x
|
flux/modules/layers.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modified: https://github.com/datnguyen-tien204
|
| 3 |
+
Flux Project: https://github.com/black-forest-labs/flux/blob/main/src/flux/modules/layers.py
|
| 4 |
+
"""
|
| 5 |
+
import math
|
| 6 |
+
import torch
|
| 7 |
+
from torch import nn, Tensor
|
| 8 |
+
from einops import rearrange
|
| 9 |
+
from flux.utils.math import attention, rope
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class EmbedND(nn.Module):
|
| 13 |
+
def __init__(self, dim: int, theta: int, axes_dim: list[int]):
|
| 14 |
+
super().__init__()
|
| 15 |
+
self.dim = dim
|
| 16 |
+
self.theta = theta
|
| 17 |
+
self.axes_dim = axes_dim
|
| 18 |
+
|
| 19 |
+
def forward(self, ids: Tensor) -> Tensor:
|
| 20 |
+
n_axes = ids.shape[-1]
|
| 21 |
+
emb = torch.cat(
|
| 22 |
+
[rope(ids[..., i], self.axes_dim[i], self.theta) for i in range(n_axes)],
|
| 23 |
+
dim=-3,
|
| 24 |
+
)
|
| 25 |
+
return emb.unsqueeze(1)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def timestep_embedding(t: Tensor, dim, max_period=10000, time_factor: float = 1000.0):
|
| 29 |
+
t = time_factor * t
|
| 30 |
+
half = dim // 2
|
| 31 |
+
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to(t.device)
|
| 32 |
+
args = t[:, None].float() * freqs[None]
|
| 33 |
+
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
| 34 |
+
if dim % 2:
|
| 35 |
+
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
| 36 |
+
if torch.is_floating_point(t):
|
| 37 |
+
embedding = embedding.to(t)
|
| 38 |
+
return embedding
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class MLPEmbedder(nn.Module):
|
| 42 |
+
def __init__(self, in_dim: int, hidden_dim: int):
|
| 43 |
+
super().__init__()
|
| 44 |
+
self.in_layer = nn.Linear(in_dim, hidden_dim, bias=True)
|
| 45 |
+
self.silu = nn.SiLU()
|
| 46 |
+
self.out_layer = nn.Linear(hidden_dim, hidden_dim, bias=True)
|
| 47 |
+
|
| 48 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 49 |
+
return self.out_layer(self.silu(self.in_layer(x)))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class RMSNorm(torch.nn.Module):
|
| 53 |
+
def __init__(self, dim: int):
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.scale = nn.Parameter(torch.ones(dim))
|
| 56 |
+
|
| 57 |
+
def forward(self, x: Tensor):
|
| 58 |
+
x_dtype = x.dtype
|
| 59 |
+
x = x.float()
|
| 60 |
+
rrms = torch.rsqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + 1e-6)
|
| 61 |
+
|
| 62 |
+
# --- DEBUG PRINT ---
|
| 63 |
+
if x.shape[-1] != self.scale.shape[0]:
|
| 64 |
+
print(f"!!! SHAPE MISMATCH ERROR !!!")
|
| 65 |
+
print(f"Input x shape: {x.shape}")
|
| 66 |
+
print(f"Scale shape: {self.scale.shape}")
|
| 67 |
+
# -------------------
|
| 68 |
+
|
| 69 |
+
return (x * rrms).to(dtype=x_dtype) * self.scale
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class QKNorm(torch.nn.Module):
|
| 73 |
+
def __init__(self, dim: int):
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.query_norm = RMSNorm(dim)
|
| 76 |
+
self.key_norm = RMSNorm(dim)
|
| 77 |
+
|
| 78 |
+
def forward(self, q: Tensor, k: Tensor, v: Tensor) -> tuple[Tensor, Tensor]:
|
| 79 |
+
q = self.query_norm(q)
|
| 80 |
+
k = self.key_norm(k)
|
| 81 |
+
return q.to(v), k.to(v)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class SelfAttention(nn.Module):
|
| 85 |
+
def __init__(self, dim: int, num_heads: int = 8, qkv_bias: bool = False):
|
| 86 |
+
super().__init__()
|
| 87 |
+
self.num_heads = num_heads
|
| 88 |
+
head_dim = dim // num_heads
|
| 89 |
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
| 90 |
+
self.norm = QKNorm(head_dim)
|
| 91 |
+
self.proj = nn.Linear(dim, dim)
|
| 92 |
+
|
| 93 |
+
def forward(self, x: Tensor, pe: Tensor) -> Tensor:
|
| 94 |
+
qkv = self.qkv(x)
|
| 95 |
+
q, k, v = rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
|
| 96 |
+
q, k = self.norm(q, k, v)
|
| 97 |
+
x = attention(q, k, v, pe=pe)
|
| 98 |
+
x = self.proj(x)
|
| 99 |
+
return x
|
flux/modules/losses.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Perceptual and adversarial loss modules for enhanced training.
|
| 3 |
+
Includes LPIPS, VGG perceptual loss, and PatchGAN discriminator.
|
| 4 |
+
Author: https://github.com/datnguyen-tien204
|
| 5 |
+
"""
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from torchvision.models import vgg16, VGG16_Weights
|
| 10 |
+
from torchvision import transforms, models
|
| 11 |
+
from typing import List, Optional
|
| 12 |
+
import lpips
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class CharbonnierLoss(nn.Module):
|
| 16 |
+
def __init__(self, eps=1e-4):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.eps = eps
|
| 19 |
+
def forward(self, x, y):
|
| 20 |
+
diff = x - y
|
| 21 |
+
return torch.mean(torch.sqrt(diff * diff + self.eps * self.eps))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class DayPreservationLoss(nn.Module):
|
| 25 |
+
def __init__(self): super().__init__()
|
| 26 |
+
def forward(self, pred_img, day_img, hour_val):
|
| 27 |
+
if isinstance(hour_val, torch.Tensor):
|
| 28 |
+
weights = []
|
| 29 |
+
for h in hour_val:
|
| 30 |
+
h = h.item() % 24.0
|
| 31 |
+
if 11.0 <= h <= 13.0: w = 1.0
|
| 32 |
+
elif 10.0 <= h < 11.0: w = (h - 10.0)
|
| 33 |
+
elif 13.0 < h <= 14.0: w = (14.0 - h)
|
| 34 |
+
else: w = 0.0
|
| 35 |
+
weights.append(w)
|
| 36 |
+
weight_tensor = torch.tensor(weights, device=pred_img.device, dtype=pred_img.dtype).view(-1, 1, 1, 1)
|
| 37 |
+
if weight_tensor.sum() > 0:
|
| 38 |
+
return (torch.abs(pred_img - day_img) * weight_tensor).mean()
|
| 39 |
+
return torch.tensor(0.0, device=pred_img.device)
|
| 40 |
+
|
| 41 |
+
class PerceptualLoss(nn.Module):
|
| 42 |
+
def __init__(self, device):
|
| 43 |
+
super().__init__()
|
| 44 |
+
vgg = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1).features.to(device).eval()
|
| 45 |
+
for param in vgg.parameters(): param.requires_grad = False
|
| 46 |
+
self.slice1 = nn.Sequential(*list(vgg[:4])).to(device)
|
| 47 |
+
self.slice2 = nn.Sequential(*list(vgg[4:9])).to(device)
|
| 48 |
+
self.normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| 49 |
+
|
| 50 |
+
def forward(self, x, y):
|
| 51 |
+
x_norm = self.normalize((x + 1.0) / 2.0)
|
| 52 |
+
y_norm = self.normalize((y + 1.0) / 2.0)
|
| 53 |
+
x_f1 = self.slice1(x_norm)
|
| 54 |
+
y_f1 = self.slice1(y_norm)
|
| 55 |
+
x_f2 = self.slice2(x_f1)
|
| 56 |
+
y_f2 = self.slice2(y_f1)
|
| 57 |
+
return (F.l1_loss(x_f1, y_f1) * 0.5 + F.l1_loss(x_f2, y_f2) * 0.5) * 0.01
|
| 58 |
+
|
| 59 |
+
class ColorMatchingLoss(nn.Module):
|
| 60 |
+
def __init__(self, device, use_perceptual=True):
|
| 61 |
+
super().__init__()
|
| 62 |
+
self.use_perceptual = use_perceptual
|
| 63 |
+
if use_perceptual: self.perceptual_loss = PerceptualLoss(device)
|
| 64 |
+
def forward(self, pred, target):
|
| 65 |
+
pred_01, target_01 = (pred + 1) / 2, (target + 1) / 2
|
| 66 |
+
mean_loss = F.mse_loss(pred_01.mean([2,3]), target_01.mean([2,3]))
|
| 67 |
+
std_loss = F.mse_loss(pred_01.std([2,3]), target_01.std([2,3]))
|
| 68 |
+
|
| 69 |
+
eps = 1e-3
|
| 70 |
+
p_c, t_c = pred_01.clamp(eps, 1), target_01.clamp(eps, 1)
|
| 71 |
+
def ratios(x): return (torch.log(x[:,0:1])-torch.log(x[:,1:2])).clamp(-3,3), (torch.log(x[:,0:1])-torch.log(x[:,2:3])).clamp(-3,3), (torch.log(x[:,1:2])-torch.log(x[:,2:3])).clamp(-3,3)
|
| 72 |
+
prg, prb, pgb = ratios(p_c)
|
| 73 |
+
trg, trb, tgb = ratios(t_c)
|
| 74 |
+
ratio_loss = (F.mse_loss(prg, trg) + F.mse_loss(prb, trb) + F.mse_loss(pgb, tgb)) / 3
|
| 75 |
+
|
| 76 |
+
loss = mean_loss * 1.0 + std_loss * 0.5 + ratio_loss * 0.3
|
| 77 |
+
if self.use_perceptual: loss += self.perceptual_loss(pred, target)
|
| 78 |
+
return loss
|
| 79 |
+
|
| 80 |
+
class ContinuityLoss(nn.Module):
|
| 81 |
+
def __init__(self, max_rate=0.15):
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.max_rate = max_rate
|
| 84 |
+
|
| 85 |
+
def forward(self, outputs, phis):
|
| 86 |
+
# outputs: [N, 3, H, W] - N can be batch size OR temporal buffer size
|
| 87 |
+
if outputs.shape[0] < 2: return torch.tensor(0.0, device=outputs.device)
|
| 88 |
+
|
| 89 |
+
# Sort by phi to ensure temporal order
|
| 90 |
+
idx = torch.argsort(phis)
|
| 91 |
+
out, phi = outputs[idx], phis[idx]
|
| 92 |
+
|
| 93 |
+
# Calculate Brightness
|
| 94 |
+
b = 0.299*out[:,0]+0.587*out[:,1]+0.114*out[:,2]
|
| 95 |
+
|
| 96 |
+
# Calculate diffs
|
| 97 |
+
diff_phi = (phi[1:] - phi[:-1])
|
| 98 |
+
|
| 99 |
+
# Filter out cases where phi is too close (avoid division by zero)
|
| 100 |
+
mask = diff_phi > 1e-4
|
| 101 |
+
if not mask.any(): return torch.tensor(0.0, device=outputs.device)
|
| 102 |
+
|
| 103 |
+
# Rate of change: |dB| / dPhi
|
| 104 |
+
dB = torch.abs(b[1:] - b[:-1])
|
| 105 |
+
dB = dB[mask] # [K, H, W]
|
| 106 |
+
dphi = diff_phi[mask].view(-1,1,1) # [K,1,1]
|
| 107 |
+
|
| 108 |
+
rate = dB / dphi
|
| 109 |
+
return torch.mean(F.relu(rate - self.max_rate).clamp(0, 0.5)**2)
|
| 110 |
+
|
| 111 |
+
class ColorContinuityLoss(nn.Module):
|
| 112 |
+
def __init__(self, max_change=0.10):
|
| 113 |
+
super().__init__()
|
| 114 |
+
self.max_change = max_change
|
| 115 |
+
|
| 116 |
+
def forward(self, outputs, phis):
|
| 117 |
+
if outputs.shape[0] < 2: return torch.tensor(0.0, device=outputs.device)
|
| 118 |
+
|
| 119 |
+
idx = torch.argsort(phis)
|
| 120 |
+
out, phi = outputs[idx], phis[idx]
|
| 121 |
+
|
| 122 |
+
diff_phi = (phi[1:] - phi[:-1])
|
| 123 |
+
mask = diff_phi > 1e-4
|
| 124 |
+
if not mask.any(): return torch.tensor(0.0, device=outputs.device)
|
| 125 |
+
|
| 126 |
+
# Rate of change: |dColor| / dPhi
|
| 127 |
+
rate = torch.abs(out[1:] - out[:-1])[mask] / diff_phi[mask].view(-1,1,1,1)
|
| 128 |
+
|
| 129 |
+
loss = 0
|
| 130 |
+
for c in range(3):
|
| 131 |
+
loss += torch.mean(F.relu(rate[:,c] - self.max_change).clamp(0, 0.3)**2)
|
| 132 |
+
return loss / 3
|
flux/modules/moe.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mixture-of-Experts (MoE) module for time-based style control.
|
| 3 |
+
Implements sparse expert selection based on time intervals.
|
| 4 |
+
Author: https://github.com/datnguyen-tien204
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
from typing import Tuple, Optional, List
|
| 11 |
+
import math
|
| 12 |
+
|
| 13 |
+
class Expert(nn.Module):
|
| 14 |
+
def __init__(self, input_dim: int, hidden_dim: int, output_dim: int):
|
| 15 |
+
super().__init__()
|
| 16 |
+
self.network = nn.Sequential(
|
| 17 |
+
nn.Linear(input_dim, hidden_dim),
|
| 18 |
+
nn.SiLU(),
|
| 19 |
+
nn.Linear(hidden_dim, hidden_dim),
|
| 20 |
+
nn.SiLU(),
|
| 21 |
+
nn.Linear(hidden_dim, output_dim)
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 25 |
+
return self.network(x)
|
| 26 |
+
|
| 27 |
+
class TimeBasedGatingNetwork(nn.Module):
|
| 28 |
+
def __init__(self, num_experts: int, input_dim: int = 2, temperature: float = 0.2,
|
| 29 |
+
custom_centers: Optional[List[float]] = None,
|
| 30 |
+
custom_widths: Optional[List[float]] = None):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.num_experts = num_experts
|
| 33 |
+
self.temperature = temperature
|
| 34 |
+
|
| 35 |
+
if custom_centers is not None:
|
| 36 |
+
centers_rad = [c * (2 * math.pi / 24.0) for c in custom_centers]
|
| 37 |
+
self.register_buffer('expert_centers', torch.tensor(centers_rad))
|
| 38 |
+
else:
|
| 39 |
+
centers = [i * (2 * math.pi / num_experts) for i in range(num_experts)]
|
| 40 |
+
self.register_buffer('expert_centers', torch.tensor(centers))
|
| 41 |
+
|
| 42 |
+
if custom_widths is not None:
|
| 43 |
+
widths_rad = [(w * (2 * math.pi / 24.0)) / 2.0 for w in custom_widths]
|
| 44 |
+
init_log_widths = torch.log(torch.tensor(widths_rad) + 1e-5)
|
| 45 |
+
self.expert_log_widths = nn.Parameter(init_log_widths)
|
| 46 |
+
else:
|
| 47 |
+
self.expert_log_widths = nn.Parameter(torch.zeros(num_experts))
|
| 48 |
+
|
| 49 |
+
self.min_width = 0.1
|
| 50 |
+
|
| 51 |
+
def forward(self, phi: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 52 |
+
phi = phi.view(-1, 1)
|
| 53 |
+
centers = self.expert_centers.view(1, -1)
|
| 54 |
+
|
| 55 |
+
diff = torch.remainder(phi - centers + math.pi, 2 * math.pi) - math.pi
|
| 56 |
+
|
| 57 |
+
widths = F.softplus(self.expert_log_widths).view(1, -1) + self.min_width
|
| 58 |
+
|
| 59 |
+
logits = -diff ** 2 / (2 * widths ** 2)
|
| 60 |
+
weights = F.softmax(logits / self.temperature, dim=-1)
|
| 61 |
+
|
| 62 |
+
return weights, logits
|
| 63 |
+
|
| 64 |
+
class MixtureOfExperts(nn.Module):
|
| 65 |
+
def __init__(self, input_dim, hidden_dim, output_dim, num_experts, k=2, temperature=0.2, noise_std=0.1,
|
| 66 |
+
custom_centers=None, custom_widths=None):
|
| 67 |
+
super().__init__()
|
| 68 |
+
self.num_experts = num_experts
|
| 69 |
+
self.k = k
|
| 70 |
+
self.noise_std = noise_std
|
| 71 |
+
|
| 72 |
+
self.experts = nn.ModuleList([
|
| 73 |
+
Expert(input_dim, hidden_dim, output_dim) for _ in range(num_experts)
|
| 74 |
+
])
|
| 75 |
+
|
| 76 |
+
self.gate = TimeBasedGatingNetwork(
|
| 77 |
+
num_experts, input_dim=2, temperature=temperature,
|
| 78 |
+
custom_centers=custom_centers, custom_widths=custom_widths
|
| 79 |
+
)
|
| 80 |
+
self.register_buffer('load_balance_loss_coef', torch.tensor(0.01))
|
| 81 |
+
|
| 82 |
+
def _add_noise(self, logits):
|
| 83 |
+
if self.training and self.noise_std > 0:
|
| 84 |
+
noise = torch.randn_like(logits) * self.noise_std
|
| 85 |
+
return logits + noise
|
| 86 |
+
return logits
|
| 87 |
+
|
| 88 |
+
def _compute_load_balance_loss(self, weights):
|
| 89 |
+
mean_weights = weights.mean(dim=0)
|
| 90 |
+
target = torch.ones_like(mean_weights) / self.num_experts
|
| 91 |
+
loss = F.mse_loss(mean_weights, target)
|
| 92 |
+
return loss * self.load_balance_loss_coef
|
| 93 |
+
|
| 94 |
+
def forward(self, x, phi, return_aux=False):
|
| 95 |
+
is_sequence = x.dim() == 3
|
| 96 |
+
if is_sequence:
|
| 97 |
+
B, L, C = x.shape
|
| 98 |
+
x = x.reshape(B * L, C)
|
| 99 |
+
phi = phi.unsqueeze(1).expand(B, L).reshape(B * L)
|
| 100 |
+
|
| 101 |
+
weights, gate_logits = self.gate(phi)
|
| 102 |
+
|
| 103 |
+
if self.training:
|
| 104 |
+
gate_logits = self._add_noise(gate_logits)
|
| 105 |
+
weights = F.softmax(gate_logits / self.gate.temperature, dim=-1)
|
| 106 |
+
|
| 107 |
+
if self.k < self.num_experts:
|
| 108 |
+
top_k_weights, top_k_indices = torch.topk(weights, self.k, dim=-1)
|
| 109 |
+
top_k_weights = top_k_weights / top_k_weights.sum(dim=-1, keepdim=True)
|
| 110 |
+
sparse_weights = torch.zeros_like(weights)
|
| 111 |
+
sparse_weights.scatter_(1, top_k_indices, top_k_weights)
|
| 112 |
+
weights = sparse_weights
|
| 113 |
+
|
| 114 |
+
expert_outputs = torch.stack([e(x) for e in self.experts], dim=1)
|
| 115 |
+
output = torch.einsum('be,beo->bo', weights, expert_outputs)
|
| 116 |
+
|
| 117 |
+
if is_sequence:
|
| 118 |
+
output = output.reshape(B, L, -1)
|
| 119 |
+
weights = weights.reshape(B, L, -1).mean(dim=1)
|
| 120 |
+
|
| 121 |
+
aux = None
|
| 122 |
+
if return_aux:
|
| 123 |
+
aux = {'load_balance_loss': self._compute_load_balance_loss(weights)}
|
| 124 |
+
|
| 125 |
+
return output, aux
|
| 126 |
+
|
| 127 |
+
class MoEStyleAdapter(nn.Module):
|
| 128 |
+
def __init__(self, style_dim, hidden_dim, num_experts, custom_centers=None, custom_widths=None):
|
| 129 |
+
super().__init__()
|
| 130 |
+
self.moe = MixtureOfExperts(
|
| 131 |
+
style_dim, hidden_dim, style_dim, num_experts, k=2, temperature=0.2, noise_std=0.1,
|
| 132 |
+
custom_centers=custom_centers, custom_widths=custom_widths
|
| 133 |
+
)
|
| 134 |
+
def forward(self, style, phi, return_aux=False):
|
| 135 |
+
adapted, aux = self.moe(style, phi, return_aux=return_aux)
|
| 136 |
+
return adapted + style, aux
|
| 137 |
+
|
| 138 |
+
class MoEFeatureProcessor(nn.Module):
|
| 139 |
+
def __init__(self, feature_dim, num_experts, expand_ratio=4, custom_centers=None, custom_widths=None):
|
| 140 |
+
super().__init__()
|
| 141 |
+
self.moe = MixtureOfExperts(
|
| 142 |
+
feature_dim, feature_dim * expand_ratio, feature_dim, num_experts, k=2, temperature=0.2, noise_std=0.1,
|
| 143 |
+
custom_centers=custom_centers, custom_widths=custom_widths
|
| 144 |
+
)
|
| 145 |
+
self.norm = nn.LayerNorm(feature_dim)
|
| 146 |
+
def forward(self, x, phi, return_aux=False):
|
| 147 |
+
normed = self.norm(x)
|
| 148 |
+
processed, aux = self.moe(normed, phi, return_aux=return_aux)
|
| 149 |
+
return x + processed, aux
|
flux/modules/normalization.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Adaptive Normalization Modules for ChronFlux
|
| 3 |
+
Includes AdaIN (Adaptive Instance Normalization) and AdaLN (Adaptive Layer Normalization)
|
| 4 |
+
"""
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
NIGHT_SKY_INTENSITY = -0.95
|
| 11 |
+
WARM_BIAS_RGB = [0.2, 0.0, -0.2]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AdaIN(nn.Module):
|
| 15 |
+
"""
|
| 16 |
+
Adaptive Instance Normalization (AdaIN) for style transfer.
|
| 17 |
+
Used for generating pseudo-style targets during training with extensive augmentation.
|
| 18 |
+
|
| 19 |
+
Reference: Huang & Belongie, "Arbitrary Style Transfer in Real-time with Adaptive Instance Normalization"
|
| 20 |
+
"""
|
| 21 |
+
def __init__(self, eps=1e-5):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.eps = eps
|
| 24 |
+
|
| 25 |
+
def calc_mean_std(self, feat):
|
| 26 |
+
size = feat.size()
|
| 27 |
+
N, C = size[:2]
|
| 28 |
+
feat_view = feat.view(N, C, -1)
|
| 29 |
+
|
| 30 |
+
feat_std, feat_mean = torch.std_mean(feat_view, dim=2, unbiased=False)
|
| 31 |
+
|
| 32 |
+
feat_std = (feat_std + self.eps).view(N, C, 1, 1)
|
| 33 |
+
feat_mean = feat_mean.view(N, C, 1, 1)
|
| 34 |
+
|
| 35 |
+
return feat_mean, feat_std
|
| 36 |
+
|
| 37 |
+
def forward(self, content, style):
|
| 38 |
+
"""
|
| 39 |
+
Apply AdaIN: transfers style statistics to content
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
content: Content features [B, C, H, W]
|
| 43 |
+
style: Style features [B, C, H, W]
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
Stylized content with style's mean and std
|
| 47 |
+
"""
|
| 48 |
+
size = content.size()
|
| 49 |
+
content_mean, content_std = self.calc_mean_std(content)
|
| 50 |
+
style_mean, style_std = self.calc_mean_std(style)
|
| 51 |
+
|
| 52 |
+
normalized = (content - content_mean) / content_std
|
| 53 |
+
stylized = normalized * style_std + style_mean
|
| 54 |
+
|
| 55 |
+
return stylized
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class AdaINTransfer(nn.Module):
|
| 59 |
+
"""
|
| 60 |
+
Extended AdaIN for pseudo-style target generation with augmentation support.
|
| 61 |
+
Used specifically for training pipeline to create robust style targets.
|
| 62 |
+
"""
|
| 63 |
+
def __init__(self, eps=1e-5):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.adain = AdaIN(eps=eps)
|
| 66 |
+
|
| 67 |
+
self.register_buffer(
|
| 68 |
+
'warm_bias_template',
|
| 69 |
+
torch.tensor(WARM_BIAS_RGB, dtype=torch.float32).view(1, 3, 1, 1)
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
def adaptive_night_transfer(self, content, style, sky_mask=None):
|
| 73 |
+
"""
|
| 74 |
+
Transfer night style to content with optional sky masking
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
content: Day images [B, C, H, W]
|
| 78 |
+
style: Night images [B, C, H, W]
|
| 79 |
+
sky_mask: Binary mask for sky regions [B, 1, H, W]
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
Night-styled content
|
| 83 |
+
"""
|
| 84 |
+
target = self.adain(content, style)
|
| 85 |
+
|
| 86 |
+
if sky_mask is not None:
|
| 87 |
+
target = target * (1 - sky_mask) + (NIGHT_SKY_INTENSITY * sky_mask)
|
| 88 |
+
|
| 89 |
+
return target.clamp(-0.95, 0.95)
|
| 90 |
+
|
| 91 |
+
def adaptive_sunset_transfer(self, content, style_night, sky_mask=None):
|
| 92 |
+
"""
|
| 93 |
+
Create sunset style by channel manipulation of night style
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
content: Day images [B, C, H, W]
|
| 97 |
+
style_night: Night images for base style [B, C, H, W]
|
| 98 |
+
sky_mask: Binary mask for sky regions [B, 1, H, W]
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
Sunset-styled content with warm tones
|
| 102 |
+
"""
|
| 103 |
+
content_mean, content_std = self.adain.calc_mean_std(content)
|
| 104 |
+
style_mean, style_std = self.adain.calc_mean_std(style_night)
|
| 105 |
+
|
| 106 |
+
mu_swap = style_mean.clone()
|
| 107 |
+
mu_swap[:, 0] = style_mean[:, 2]
|
| 108 |
+
mu_swap[:, 2] = style_mean[:, 0]
|
| 109 |
+
|
| 110 |
+
std_swap = style_std.clone()
|
| 111 |
+
std_swap[:, 0] = style_std[:, 2]
|
| 112 |
+
std_swap[:, 2] = style_std[:, 0]
|
| 113 |
+
|
| 114 |
+
B = content.shape[0]
|
| 115 |
+
warm_bias = self.warm_bias_template.to(content.dtype).expand(B, 3, 1, 1)
|
| 116 |
+
|
| 117 |
+
mu_final = mu_swap * 0.7 + warm_bias * 0.3
|
| 118 |
+
std_final = std_swap * 1.4
|
| 119 |
+
|
| 120 |
+
target = (content - content_mean) / content_std * std_final + mu_final
|
| 121 |
+
|
| 122 |
+
if sky_mask is not None:
|
| 123 |
+
sky_color = mu_final.view(B, 3, 1, 1)
|
| 124 |
+
target = target * (1 - sky_mask) + (target * 0.4 + sky_color * 0.6) * sky_mask
|
| 125 |
+
|
| 126 |
+
return target.clamp(-0.95, 0.95)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class AdaLNModulation(nn.Module):
|
| 130 |
+
"""
|
| 131 |
+
Adaptive Layer Normalization (AdaLN) Modulation.
|
| 132 |
+
Generates scale, shift, and gate parameters for layer-wise adaptive normalization.
|
| 133 |
+
Provides better control over style consistency and layer-wise adjustments.
|
| 134 |
+
"""
|
| 135 |
+
def __init__(self, hidden_size, components=6):
|
| 136 |
+
super().__init__()
|
| 137 |
+
self.silu = nn.SiLU()
|
| 138 |
+
self.linear = nn.Linear(hidden_size, components * hidden_size, bias=True)
|
| 139 |
+
self.components = components
|
| 140 |
+
|
| 141 |
+
nn.init.zeros_(self.linear.weight)
|
| 142 |
+
nn.init.zeros_(self.linear.bias)
|
| 143 |
+
|
| 144 |
+
def forward(self, vec):
|
| 145 |
+
"""
|
| 146 |
+
Generate modulation parameters
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
vec: Conditioning vector [B, hidden_size]
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
Tuple of (shift, scale, gate) parameters for each component
|
| 153 |
+
"""
|
| 154 |
+
chunk = self.linear(self.silu(vec))
|
| 155 |
+
return chunk.chunk(self.components, dim=1)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class AdaLNBlock(nn.Module):
|
| 159 |
+
"""
|
| 160 |
+
Base AdaLN normalization block with scale, shift, and gate.
|
| 161 |
+
Can be used as a building block for various architectures.
|
| 162 |
+
"""
|
| 163 |
+
def __init__(self, hidden_size, use_gate=True):
|
| 164 |
+
super().__init__()
|
| 165 |
+
self.norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 166 |
+
self.use_gate = use_gate
|
| 167 |
+
|
| 168 |
+
def forward(self, x, scale, shift, gate=None):
|
| 169 |
+
"""
|
| 170 |
+
Apply AdaLN: normalize, then scale and shift, optionally gate
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
x: Input features [B, L, D]
|
| 174 |
+
scale: Scale parameter [B, D]
|
| 175 |
+
shift: Shift parameter [B, D]
|
| 176 |
+
gate: Optional gate parameter [B, D]
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
Modulated features
|
| 180 |
+
"""
|
| 181 |
+
x_norm = self.norm(x)
|
| 182 |
+
x_mod = x_norm * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
|
| 183 |
+
|
| 184 |
+
if self.use_gate and gate is not None:
|
| 185 |
+
x_mod = gate.unsqueeze(1) * x_mod
|
| 186 |
+
|
| 187 |
+
return x_mod
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class AdaLNZero(nn.Module):
|
| 191 |
+
"""
|
| 192 |
+
AdaLN-Zero: AdaLN with zero-initialized output scaling.
|
| 193 |
+
Particularly useful for residual connections and stable training.
|
| 194 |
+
"""
|
| 195 |
+
def __init__(self, hidden_size):
|
| 196 |
+
super().__init__()
|
| 197 |
+
self.norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 198 |
+
self.scale = nn.Parameter(torch.zeros(hidden_size))
|
| 199 |
+
self.shift = nn.Parameter(torch.zeros(hidden_size))
|
| 200 |
+
|
| 201 |
+
def forward(self, x, modulation_scale, modulation_shift):
|
| 202 |
+
"""
|
| 203 |
+
Apply AdaLN with zero-initialization
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
x: Input features [B, L, D]
|
| 207 |
+
modulation_scale: Additional scale from conditioning [B, D]
|
| 208 |
+
modulation_shift: Additional shift from conditioning [B, D]
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
Modulated features
|
| 212 |
+
"""
|
| 213 |
+
x_norm = self.norm(x)
|
| 214 |
+
x_mod = x_norm * (self.scale + modulation_scale.unsqueeze(1)) + (self.shift + modulation_shift.unsqueeze(1))
|
| 215 |
+
return x_mod
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class StyleAdaLN(nn.Module):
|
| 219 |
+
"""
|
| 220 |
+
Style-aware AdaLN that combines style and conditioning vectors.
|
| 221 |
+
Particularly useful for style transfer with additional controls.
|
| 222 |
+
"""
|
| 223 |
+
def __init__(self, hidden_size, style_dim):
|
| 224 |
+
super().__init__()
|
| 225 |
+
self.style_proj = nn.Linear(style_dim, hidden_size)
|
| 226 |
+
self.adaln = AdaLNModulation(hidden_size, components=2)
|
| 227 |
+
self.norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 228 |
+
|
| 229 |
+
def forward(self, x, style_vec, cond_vec=None):
|
| 230 |
+
"""
|
| 231 |
+
Apply style-aware normalization
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
x: Input features [B, L, D]
|
| 235 |
+
style_vec: Style vector [B, style_dim]
|
| 236 |
+
cond_vec: Optional additional conditioning [B, D]
|
| 237 |
+
|
| 238 |
+
Returns:
|
| 239 |
+
Style-modulated features
|
| 240 |
+
"""
|
| 241 |
+
style_emb = self.style_proj(style_vec)
|
| 242 |
+
|
| 243 |
+
if cond_vec is not None:
|
| 244 |
+
combined = style_emb + cond_vec
|
| 245 |
+
else:
|
| 246 |
+
combined = style_emb
|
| 247 |
+
|
| 248 |
+
scale, shift = self.adaln(combined)
|
| 249 |
+
|
| 250 |
+
x_norm = self.norm(x)
|
| 251 |
+
x_mod = x_norm * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
|
| 252 |
+
|
| 253 |
+
return x_mod
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
__all__ = [
|
| 257 |
+
'AdaIN',
|
| 258 |
+
'AdaINTransfer',
|
| 259 |
+
'AdaLNModulation',
|
| 260 |
+
'AdaLNBlock',
|
| 261 |
+
'AdaLNZero',
|
| 262 |
+
'StyleAdaLN',
|
| 263 |
+
]
|
flux/modules/sampling.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
State-of-the-art sampling techniques (2024-2025).
|
| 3 |
+
Includes Flow Matching with Optimal Transport and Consistency Models.
|
| 4 |
+
"""
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from typing import Optional, Callable
|
| 9 |
+
import math
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class FlowMatching:
|
| 13 |
+
"""
|
| 14 |
+
Flow Matching with Optimal Transport for improved trajectory learning.
|
| 15 |
+
Based on recent advances in continuous normalizing flows.
|
| 16 |
+
"""
|
| 17 |
+
def __init__(self, sigma_min: float = 0.001):
|
| 18 |
+
"""
|
| 19 |
+
Args:
|
| 20 |
+
sigma_min: Minimum noise level for numerical stability
|
| 21 |
+
"""
|
| 22 |
+
self.sigma_min = sigma_min
|
| 23 |
+
|
| 24 |
+
def compute_optimal_transport_path(self, x0: torch.Tensor, x1: torch.Tensor,
|
| 25 |
+
t: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 26 |
+
"""
|
| 27 |
+
Compute optimal transport path from noise (x0) to data (x1).
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
x0: Source distribution (noise) [B, ...]
|
| 31 |
+
x1: Target distribution (data) [B, ...]
|
| 32 |
+
t: Time steps [B] in range [0, 1]
|
| 33 |
+
Returns:
|
| 34 |
+
x_t: Interpolated samples
|
| 35 |
+
v_t: Vector field (velocity)
|
| 36 |
+
"""
|
| 37 |
+
t = t.view(-1, *([1] * (x0.dim() - 1)))
|
| 38 |
+
|
| 39 |
+
# Optimal transport interpolation (linear for Gaussian)
|
| 40 |
+
x_t = (1 - t) * x0 + t * x1
|
| 41 |
+
|
| 42 |
+
# Velocity is the derivative: v = dx/dt = x1 - x0
|
| 43 |
+
v_t = x1 - x0
|
| 44 |
+
|
| 45 |
+
return x_t, v_t
|
| 46 |
+
|
| 47 |
+
def compute_conditional_flow(self, x0: torch.Tensor, x1: torch.Tensor,
|
| 48 |
+
t: torch.Tensor, sigma: Optional[float] = None) -> tuple[torch.Tensor, torch.Tensor]:
|
| 49 |
+
"""
|
| 50 |
+
Compute conditional flow with added noise for robustness.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
x0: Noise [B, ...]
|
| 54 |
+
x1: Data [B, ...]
|
| 55 |
+
t: Time [B]
|
| 56 |
+
sigma: Optional noise level (defaults to sigma_min * (1-t))
|
| 57 |
+
Returns:
|
| 58 |
+
x_t: Noisy interpolation
|
| 59 |
+
v_t: Target velocity
|
| 60 |
+
"""
|
| 61 |
+
if sigma is None:
|
| 62 |
+
sigma = self.sigma_min * (1 - t.view(-1, *([1] * (x0.dim() - 1))))
|
| 63 |
+
else:
|
| 64 |
+
sigma = sigma * torch.ones_like(t).view(-1, *([1] * (x0.dim() - 1)))
|
| 65 |
+
|
| 66 |
+
# Conditional path
|
| 67 |
+
x_t, v_t = self.compute_optimal_transport_path(x0, x1, t)
|
| 68 |
+
|
| 69 |
+
# Add Gaussian noise for conditional flow matching
|
| 70 |
+
eps = torch.randn_like(x_t)
|
| 71 |
+
x_t = x_t + sigma * eps
|
| 72 |
+
|
| 73 |
+
return x_t, v_t
|
| 74 |
+
|
| 75 |
+
def sample(self, model: Callable, x_init: torch.Tensor,
|
| 76 |
+
num_steps: int = 50, **model_kwargs) -> torch.Tensor:
|
| 77 |
+
"""
|
| 78 |
+
Sample using Euler method for ODE solving.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
model: Velocity prediction model
|
| 82 |
+
x_init: Initial noise [B, ...]
|
| 83 |
+
num_steps: Number of integration steps
|
| 84 |
+
**model_kwargs: Additional arguments for model
|
| 85 |
+
Returns:
|
| 86 |
+
x_final: Generated samples
|
| 87 |
+
"""
|
| 88 |
+
x = x_init
|
| 89 |
+
dt = 1.0 / num_steps
|
| 90 |
+
|
| 91 |
+
for step in range(num_steps):
|
| 92 |
+
t_current = torch.full((x.shape[0],), step / num_steps,
|
| 93 |
+
device=x.device, dtype=x.dtype)
|
| 94 |
+
|
| 95 |
+
# Predict velocity
|
| 96 |
+
with torch.no_grad():
|
| 97 |
+
v = model(x, timesteps=t_current, **model_kwargs)
|
| 98 |
+
|
| 99 |
+
# Euler step
|
| 100 |
+
x = x + v * dt
|
| 101 |
+
|
| 102 |
+
return x
|
| 103 |
+
|
| 104 |
+
def sample_heun(self, model: Callable, x_init: torch.Tensor,
|
| 105 |
+
num_steps: int = 50, **model_kwargs) -> torch.Tensor:
|
| 106 |
+
"""
|
| 107 |
+
Higher-order sampling using Heun's method (2nd order).
|
| 108 |
+
More accurate but requires 2x model evaluations.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
model: Velocity prediction model
|
| 112 |
+
x_init: Initial noise
|
| 113 |
+
num_steps: Number of steps
|
| 114 |
+
**model_kwargs: Model arguments
|
| 115 |
+
Returns:
|
| 116 |
+
x_final: Generated samples
|
| 117 |
+
"""
|
| 118 |
+
x = x_init
|
| 119 |
+
dt = 1.0 / num_steps
|
| 120 |
+
|
| 121 |
+
for step in range(num_steps):
|
| 122 |
+
t_current = torch.full((x.shape[0],), step / num_steps,
|
| 123 |
+
device=x.device, dtype=x.dtype)
|
| 124 |
+
|
| 125 |
+
# First stage
|
| 126 |
+
with torch.no_grad():
|
| 127 |
+
v1 = model(x, timesteps=t_current, **model_kwargs)
|
| 128 |
+
|
| 129 |
+
x_temp = x + v1 * dt
|
| 130 |
+
|
| 131 |
+
# Second stage
|
| 132 |
+
t_next = torch.full((x.shape[0],), (step + 1) / num_steps,
|
| 133 |
+
device=x.device, dtype=x.dtype)
|
| 134 |
+
|
| 135 |
+
with torch.no_grad():
|
| 136 |
+
v2 = model(x_temp, timesteps=t_next, **model_kwargs)
|
| 137 |
+
|
| 138 |
+
# Heun's method: average of two velocities
|
| 139 |
+
x = x + (v1 + v2) * 0.5 * dt
|
| 140 |
+
|
| 141 |
+
return x
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class ConsistencyModel:
|
| 145 |
+
"""
|
| 146 |
+
Consistency Models for fast, single-step generation.
|
| 147 |
+
Based on "Consistency Models" (Song et al., 2023).
|
| 148 |
+
"""
|
| 149 |
+
def __init__(self, sigma_min: float = 0.002, sigma_max: float = 80.0,
|
| 150 |
+
rho: float = 7.0):
|
| 151 |
+
"""
|
| 152 |
+
Args:
|
| 153 |
+
sigma_min: Minimum noise level
|
| 154 |
+
sigma_max: Maximum noise level
|
| 155 |
+
rho: Schedule parameter
|
| 156 |
+
"""
|
| 157 |
+
self.sigma_min = sigma_min
|
| 158 |
+
self.sigma_max = sigma_max
|
| 159 |
+
self.rho = rho
|
| 160 |
+
|
| 161 |
+
def get_schedule(self, num_steps: int, device: torch.device) -> torch.Tensor:
|
| 162 |
+
"""
|
| 163 |
+
Get noise schedule for consistency training.
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
num_steps: Number of discretization steps
|
| 167 |
+
device: Device for tensors
|
| 168 |
+
Returns:
|
| 169 |
+
sigmas: Noise levels [num_steps + 1]
|
| 170 |
+
"""
|
| 171 |
+
rho_inv = 1.0 / self.rho
|
| 172 |
+
steps = torch.arange(num_steps + 1, device=device)
|
| 173 |
+
sigmas = (self.sigma_max ** rho_inv +
|
| 174 |
+
steps / num_steps * (self.sigma_min ** rho_inv - self.sigma_max ** rho_inv)) ** self.rho
|
| 175 |
+
return sigmas
|
| 176 |
+
|
| 177 |
+
def consistency_function(self, model: Callable, x: torch.Tensor,
|
| 178 |
+
sigma: torch.Tensor, **model_kwargs) -> torch.Tensor:
|
| 179 |
+
"""
|
| 180 |
+
Apply consistency function: maps (x, sigma) to denoised output.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
model: Denoising model
|
| 184 |
+
x: Noisy input
|
| 185 |
+
sigma: Noise level
|
| 186 |
+
**model_kwargs: Model arguments
|
| 187 |
+
Returns:
|
| 188 |
+
denoised: Consistency function output
|
| 189 |
+
"""
|
| 190 |
+
# Skip connection for sigma_min
|
| 191 |
+
c_skip = self.sigma_min ** 2 / (sigma ** 2 + self.sigma_min ** 2)
|
| 192 |
+
c_out = sigma * self.sigma_min / (sigma ** 2 + self.sigma_min ** 2).sqrt()
|
| 193 |
+
|
| 194 |
+
# Predict denoised output
|
| 195 |
+
with torch.no_grad():
|
| 196 |
+
# Convert sigma to timestep for model
|
| 197 |
+
t = sigma / self.sigma_max
|
| 198 |
+
model_out = model(x, timesteps=t, **model_kwargs)
|
| 199 |
+
|
| 200 |
+
# Apply consistency function
|
| 201 |
+
denoised = c_skip * x + c_out * model_out
|
| 202 |
+
|
| 203 |
+
return denoised
|
| 204 |
+
|
| 205 |
+
def sample_onestep(self, model: Callable, shape: tuple,
|
| 206 |
+
device: torch.device, dtype: torch.dtype,
|
| 207 |
+
**model_kwargs) -> torch.Tensor:
|
| 208 |
+
"""
|
| 209 |
+
Single-step generation from noise to data.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
model: Consistency model
|
| 213 |
+
shape: Output shape
|
| 214 |
+
device: Device
|
| 215 |
+
dtype: Data type
|
| 216 |
+
**model_kwargs: Model arguments
|
| 217 |
+
Returns:
|
| 218 |
+
samples: Generated samples
|
| 219 |
+
"""
|
| 220 |
+
# Sample from prior (high noise)
|
| 221 |
+
x = torch.randn(*shape, device=device, dtype=dtype) * self.sigma_max
|
| 222 |
+
sigma = torch.full((shape[0],), self.sigma_max, device=device, dtype=dtype)
|
| 223 |
+
|
| 224 |
+
# One-step denoising
|
| 225 |
+
samples = self.consistency_function(model, x, sigma, **model_kwargs)
|
| 226 |
+
|
| 227 |
+
return samples
|
| 228 |
+
|
| 229 |
+
def sample_multistep(self, model: Callable, shape: tuple,
|
| 230 |
+
num_steps: int, device: torch.device,
|
| 231 |
+
dtype: torch.dtype, **model_kwargs) -> torch.Tensor:
|
| 232 |
+
"""
|
| 233 |
+
Multi-step generation for better quality.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
model: Consistency model
|
| 237 |
+
shape: Output shape
|
| 238 |
+
num_steps: Number of steps
|
| 239 |
+
device: Device
|
| 240 |
+
dtype: Data type
|
| 241 |
+
**model_kwargs: Model arguments
|
| 242 |
+
Returns:
|
| 243 |
+
samples: Generated samples
|
| 244 |
+
"""
|
| 245 |
+
# Get noise schedule
|
| 246 |
+
sigmas = self.get_schedule(num_steps, device)
|
| 247 |
+
|
| 248 |
+
# Start from maximum noise
|
| 249 |
+
x = torch.randn(*shape, device=device, dtype=dtype) * self.sigma_max
|
| 250 |
+
|
| 251 |
+
# Iterative refinement
|
| 252 |
+
for i in range(num_steps, 0, -1):
|
| 253 |
+
sigma_current = sigmas[i]
|
| 254 |
+
sigma_next = sigmas[i - 1]
|
| 255 |
+
|
| 256 |
+
# Denoise to current level
|
| 257 |
+
x = self.consistency_function(
|
| 258 |
+
model, x,
|
| 259 |
+
torch.full((shape[0],), sigma_current, device=device, dtype=dtype),
|
| 260 |
+
**model_kwargs
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# Add noise for next iteration (except last)
|
| 264 |
+
if i > 1:
|
| 265 |
+
noise = torch.randn_like(x)
|
| 266 |
+
x = x + noise * (sigma_next ** 2 - self.sigma_min ** 2).sqrt()
|
| 267 |
+
|
| 268 |
+
return x
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
class ClassifierFreeGuidance:
|
| 272 |
+
"""
|
| 273 |
+
Enhanced Classifier-Free Guidance for better conditioning.
|
| 274 |
+
"""
|
| 275 |
+
def __init__(self, guidance_scale: float = 7.5, min_scale: float = 1.0):
|
| 276 |
+
"""
|
| 277 |
+
Args:
|
| 278 |
+
guidance_scale: Guidance strength
|
| 279 |
+
min_scale: Minimum guidance scale
|
| 280 |
+
"""
|
| 281 |
+
self.guidance_scale = guidance_scale
|
| 282 |
+
self.min_scale = min_scale
|
| 283 |
+
|
| 284 |
+
def apply_guidance(self, cond_out: torch.Tensor, uncond_out: torch.Tensor,
|
| 285 |
+
guidance_scale: Optional[float] = None) -> torch.Tensor:
|
| 286 |
+
"""
|
| 287 |
+
Apply classifier-free guidance to model outputs.
|
| 288 |
+
|
| 289 |
+
Args:
|
| 290 |
+
cond_out: Conditional model output
|
| 291 |
+
uncond_out: Unconditional model output
|
| 292 |
+
guidance_scale: Optional override for guidance scale
|
| 293 |
+
Returns:
|
| 294 |
+
guided_out: Guided output
|
| 295 |
+
"""
|
| 296 |
+
scale = guidance_scale if guidance_scale is not None else self.guidance_scale
|
| 297 |
+
scale = max(scale, self.min_scale)
|
| 298 |
+
|
| 299 |
+
# CFG formula: out = uncond + scale * (cond - uncond)
|
| 300 |
+
guided_out = uncond_out + scale * (cond_out - uncond_out)
|
| 301 |
+
|
| 302 |
+
return guided_out
|
| 303 |
+
|
| 304 |
+
def apply_dynamic_guidance(self, cond_out: torch.Tensor, uncond_out: torch.Tensor,
|
| 305 |
+
t: torch.Tensor, min_t: float = 0.0,
|
| 306 |
+
max_t: float = 1.0) -> torch.Tensor:
|
| 307 |
+
"""
|
| 308 |
+
Apply time-dependent guidance that decreases over time.
|
| 309 |
+
|
| 310 |
+
Args:
|
| 311 |
+
cond_out: Conditional output
|
| 312 |
+
uncond_out: Unconditional output
|
| 313 |
+
t: Time steps [B]
|
| 314 |
+
min_t: Time to start decreasing guidance
|
| 315 |
+
max_t: Time to reach minimum guidance
|
| 316 |
+
Returns:
|
| 317 |
+
guided_out: Dynamically guided output
|
| 318 |
+
"""
|
| 319 |
+
# Linearly decrease guidance scale from max to min over time
|
| 320 |
+
t_normalized = (t - min_t) / (max_t - min_t)
|
| 321 |
+
t_normalized = t_normalized.clamp(0, 1)
|
| 322 |
+
|
| 323 |
+
# Scale goes from guidance_scale to min_scale
|
| 324 |
+
scale = self.guidance_scale + (self.min_scale - self.guidance_scale) * t_normalized
|
| 325 |
+
scale = scale.view(-1, *([1] * (cond_out.dim() - 1)))
|
| 326 |
+
|
| 327 |
+
guided_out = uncond_out + scale * (cond_out - uncond_out)
|
| 328 |
+
|
| 329 |
+
return guided_out
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
class AdaptiveSampler:
|
| 333 |
+
"""
|
| 334 |
+
Adaptive sampling that combines multiple techniques for optimal quality/speed.
|
| 335 |
+
"""
|
| 336 |
+
def __init__(self, use_flow_matching: bool = True,
|
| 337 |
+
use_consistency: bool = False,
|
| 338 |
+
use_cfg: bool = True):
|
| 339 |
+
self.use_flow_matching = use_flow_matching
|
| 340 |
+
self.use_consistency = use_consistency
|
| 341 |
+
self.use_cfg = use_cfg
|
| 342 |
+
|
| 343 |
+
if use_flow_matching:
|
| 344 |
+
self.flow_matching = FlowMatching()
|
| 345 |
+
if use_consistency:
|
| 346 |
+
self.consistency_model = ConsistencyModel()
|
| 347 |
+
if use_cfg:
|
| 348 |
+
self.cfg = ClassifierFreeGuidance()
|
| 349 |
+
|
| 350 |
+
def sample(self, model: Callable, x_init: torch.Tensor,
|
| 351 |
+
num_steps: int = 50, method: str = "heun",
|
| 352 |
+
cfg_scale: Optional[float] = None,
|
| 353 |
+
**model_kwargs) -> torch.Tensor:
|
| 354 |
+
"""
|
| 355 |
+
Adaptive sampling with multiple techniques.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
model: Model for sampling
|
| 359 |
+
x_init: Initial noise
|
| 360 |
+
num_steps: Number of steps
|
| 361 |
+
method: Sampling method ("euler", "heun", "consistency")
|
| 362 |
+
cfg_scale: Optional CFG scale
|
| 363 |
+
**model_kwargs: Model arguments
|
| 364 |
+
Returns:
|
| 365 |
+
samples: Generated samples
|
| 366 |
+
"""
|
| 367 |
+
if method == "consistency" and self.use_consistency:
|
| 368 |
+
return self.consistency_model.sample_multistep(
|
| 369 |
+
model, x_init.shape, num_steps,
|
| 370 |
+
x_init.device, x_init.dtype, **model_kwargs
|
| 371 |
+
)
|
| 372 |
+
elif method == "heun" and self.use_flow_matching:
|
| 373 |
+
return self.flow_matching.sample_heun(
|
| 374 |
+
model, x_init, num_steps, **model_kwargs
|
| 375 |
+
)
|
| 376 |
+
else: # euler
|
| 377 |
+
return self.flow_matching.sample(
|
| 378 |
+
model, x_init, num_steps, **model_kwargs
|
| 379 |
+
)
|
flux/utils/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
import numpy as np
|
| 4 |
+
from flux.utils import logger_utils
|
flux/utils/chronflux_datasets.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Author: https://github.com/datnguyen-tien204
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import numpy as np
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch.utils.data import Dataset
|
| 11 |
+
from torchvision import transforms
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class DayNightDataset(Dataset):
|
| 15 |
+
def __init__(self, day_dir, night_dir, size=384):
|
| 16 |
+
self.day_files = [os.path.join(day_dir, f) for f in os.listdir(day_dir) if f.endswith(('.jpg', '.png'))]
|
| 17 |
+
self.night_files = [os.path.join(night_dir, f) for f in os.listdir(night_dir) if f.endswith(('.jpg', '.png'))]
|
| 18 |
+
self.transform = transforms.Compose(
|
| 19 |
+
[transforms.Resize((size, size)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
|
| 20 |
+
|
| 21 |
+
def __len__(self): return len(self.day_files)
|
| 22 |
+
|
| 23 |
+
def __getitem__(self, idx):
|
| 24 |
+
day_img = Image.open(self.day_files[idx]).convert('RGB')
|
| 25 |
+
night_img = Image.open(np.random.choice(self.night_files)).convert('RGB')
|
| 26 |
+
return self.transform(day_img), self.transform(night_img)
|
flux/utils/logger_utils.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ConsoleLogger:
|
| 2 |
+
HEADER = '\033[95m'
|
| 3 |
+
BLUE = '\033[94m'
|
| 4 |
+
CYAN = '\033[96m'
|
| 5 |
+
GREEN = '\033[92m'
|
| 6 |
+
WARNING = '\033[93m'
|
| 7 |
+
FAIL = '\033[91m'
|
| 8 |
+
ENDC = '\033[0m'
|
| 9 |
+
BOLD = '\033[1m'
|
| 10 |
+
UNDERLINE = '\033[4m'
|
| 11 |
+
|
| 12 |
+
@staticmethod
|
| 13 |
+
def info(msg): print(f"{ConsoleLogger.GREEN}{ConsoleLogger.BOLD}[INFO]{ConsoleLogger.ENDC} {msg}")
|
| 14 |
+
@staticmethod
|
| 15 |
+
def step(msg): print(f"{ConsoleLogger.CYAN}[STEP]{ConsoleLogger.ENDC} {msg}")
|
| 16 |
+
@staticmethod
|
| 17 |
+
def warn(msg): print(f"{ConsoleLogger.WARNING}{ConsoleLogger.BOLD}[WARN]{ConsoleLogger.ENDC} {msg}")
|
| 18 |
+
@staticmethod
|
| 19 |
+
def section(msg): print(f"\n{ConsoleLogger.HEADER}{ConsoleLogger.BOLD}{'='*60}\n{msg}\n{'='*60}{ConsoleLogger.ENDC}")
|
flux/utils/math.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Author: https://github.com/datnguyen-tien204
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from einops import rearrange
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
from flash_attn import flash_attn_func
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def attention(q: Tensor, k: Tensor, v: Tensor) -> Tensor:
|
| 12 |
+
# Ensure tensors are in bf16 or fp16 for FlashAttention =====
|
| 13 |
+
original_dtype = q.dtype
|
| 14 |
+
if q.dtype not in [torch.float16, torch.bfloat16]:
|
| 15 |
+
q = q.to(torch.bfloat16)
|
| 16 |
+
k = k.to(torch.bfloat16)
|
| 17 |
+
v = v.to(torch.bfloat16)
|
| 18 |
+
# ======================================================================
|
| 19 |
+
|
| 20 |
+
x = flash_attn_func(q, k, v, dropout_p=0.0, softmax_scale=None, causal=False)
|
| 21 |
+
x = rearrange(x, "B L H D -> B L (H D)")
|
| 22 |
+
|
| 23 |
+
# Convert back to original dtype if needed =====
|
| 24 |
+
if original_dtype not in [torch.float16, torch.bfloat16]:
|
| 25 |
+
x = x.to(original_dtype)
|
| 26 |
+
# ==========================================================
|
| 27 |
+
|
| 28 |
+
return x
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def rope(pos: Tensor, dim: int, theta: int) -> Tensor:
|
| 32 |
+
assert dim % 2 == 0
|
| 33 |
+
scale = torch.arange(0, dim, 2, dtype=torch.float32, device=pos.device) / dim
|
| 34 |
+
omega = 1.0 / (theta ** scale)
|
| 35 |
+
out = torch.einsum("...n,d->...nd", pos, omega)
|
| 36 |
+
out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1)
|
| 37 |
+
out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2)
|
| 38 |
+
return out.float()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor) -> tuple[Tensor, Tensor]:
|
| 42 |
+
xq_ = xq.float().reshape(*xq.shape[:-1], -1, 1, 2)
|
| 43 |
+
xk_ = xk.float().reshape(*xk.shape[:-1], -1, 1, 2)
|
| 44 |
+
|
| 45 |
+
xq_out = freqs_cis[..., 0] * xq_[..., 0] + freqs_cis[..., 1] * xq_[..., 1]
|
| 46 |
+
xk_out = freqs_cis[..., 0] * xk_[..., 0] + freqs_cis[..., 1] * xk_[..., 1]
|
| 47 |
+
|
| 48 |
+
return xq_out.reshape(*xq.shape).type_as(xq), xk_out.reshape(*xk.shape).type_as(xk)
|
flux/utils/target_generation.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Author: https://github.com/datnguyen-tien204
|
| 3 |
+
"""
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import math
|
| 7 |
+
|
| 8 |
+
PI = math.pi
|
| 9 |
+
|
| 10 |
+
def symmetric_brightness_curve(hour):
|
| 11 |
+
return 0.035 + (1.0 - 0.035) * (0.5 - 0.5 * np.cos(2 * PI * (hour / 24.0)))
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def rgb_curve_original(hour):
|
| 15 |
+
t = (hour % 24.0) / 24.0
|
| 16 |
+
sunset = np.exp(-((t - 0.75) / 0.08) ** 2)
|
| 17 |
+
sunrise = np.exp(-((t - 0.25) / 0.08) ** 2)
|
| 18 |
+
night = np.exp(-((t - 0.0) / 0.10) ** 2) + np.exp(-((t - 1.0) / 0.10) ** 2)
|
| 19 |
+
noon = np.exp(-((t - 0.5) / 0.12) ** 2)
|
| 20 |
+
return [1.0 + 0.4 * sunset + 0.22 * sunrise - 0.35 * night,
|
| 21 |
+
1.0 - 0.1 * sunset - 0.06 * night,
|
| 22 |
+
1.0 - 0.5 * sunset + 0.3 * night + 0.15 * noon]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def create_target_final_v2(day_img, style_img, depth_map, hour_val, adain_module, style_stats):
|
| 26 |
+
device, dtype, B = day_img.device, day_img.dtype, day_img.shape[0]
|
| 27 |
+
img_original = (day_img + 1.0) / 2.0
|
| 28 |
+
target_list = []
|
| 29 |
+
|
| 30 |
+
for i in range(B):
|
| 31 |
+
h = hour_val[i].item()
|
| 32 |
+
bright = symmetric_brightness_curve(h)
|
| 33 |
+
rgb = rgb_curve_original(h)
|
| 34 |
+
|
| 35 |
+
tint = torch.tensor(rgb, device=device, dtype=dtype).view(1, 3, 1, 1)
|
| 36 |
+
tint = tint * (0.6 + 0.4 * style_stats["mean"][i].view(1, 3, 1, 1))
|
| 37 |
+
|
| 38 |
+
img_i = img_original[i:i + 1]
|
| 39 |
+
target_base = img_i * tint
|
| 40 |
+
|
| 41 |
+
if bright < 0.8:
|
| 42 |
+
adain_out = (adain_module(day_img[i:i + 1], style_img[i:i + 1]) + 1.0) / 2.0
|
| 43 |
+
night_int = (1.0 - bright) ** 0.8 * 0.85
|
| 44 |
+
|
| 45 |
+
d_i = depth_map[i:i + 1]
|
| 46 |
+
d_norm = (d_i - d_i.min()) / (d_i.max() - d_i.min() + 1e-6)
|
| 47 |
+
sky_mask = torch.sigmoid((0.15 - d_norm) * 10.0)
|
| 48 |
+
|
| 49 |
+
blend = night_int * (0.7 + 0.3 * sky_mask)
|
| 50 |
+
target_mixed = target_base * (1 - blend) + adain_out * blend
|
| 51 |
+
else:
|
| 52 |
+
target_mixed = target_base
|
| 53 |
+
sky_mask = torch.zeros_like(target_mixed[:, :1])
|
| 54 |
+
|
| 55 |
+
target_calc = target_mixed * bright
|
| 56 |
+
if bright < 0.20:
|
| 57 |
+
target_calc += img_i.mean(1, keepdim=True) * tint * 0.03
|
| 58 |
+
if bright < 0.8: target_calc *= (1.0 - sky_mask * 0.5)
|
| 59 |
+
|
| 60 |
+
noon_w = torch.tensor(np.exp(-(abs(h - 12.0) ** 2) / 8.0), device=device, dtype=dtype)
|
| 61 |
+
target_list.append(target_calc * (1 - noon_w) + img_i * noon_w)
|
| 62 |
+
|
| 63 |
+
return (torch.cat(target_list, dim=0) * 2.0 - 1.0).clamp(-1, 1)
|
flux/utils/util.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import torch
|
| 5 |
+
from einops import rearrange
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from safetensors.torch import load_file as load_sft
|
| 8 |
+
from huggingface_hub import hf_hub_download
|
| 9 |
+
from flux.modules.autoencoder import AutoEncoder, AutoEncoderParams
|
| 10 |
+
|
| 11 |
+
CHECKPOINTS_DIR = Path("checkpoints")
|
| 12 |
+
CHECKPOINTS_DIR.mkdir(exist_ok=True)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class VAEConfig:
|
| 17 |
+
repo_id: str
|
| 18 |
+
repo_ae: str
|
| 19 |
+
ae_params: AutoEncoderParams
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
flux_vae_config = VAEConfig(
|
| 23 |
+
repo_id="black-forest-labs/FLUX.1-dev",
|
| 24 |
+
repo_ae="ae.safetensors",
|
| 25 |
+
ae_params=AutoEncoderParams(
|
| 26 |
+
resolution=64,
|
| 27 |
+
in_channels=3,
|
| 28 |
+
ch=128,
|
| 29 |
+
out_ch=3,
|
| 30 |
+
ch_mult=[1, 2, 4, 4],
|
| 31 |
+
num_res_blocks=2,
|
| 32 |
+
z_channels=16,
|
| 33 |
+
scale_factor=0.3611,
|
| 34 |
+
shift_factor=0.1159,
|
| 35 |
+
),
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def get_checkpoint_path(filename: str, repo_id: str) -> Path:
|
| 40 |
+
local_path = CHECKPOINTS_DIR / filename
|
| 41 |
+
if not local_path.exists():
|
| 42 |
+
print(f"Downloading {filename} from {repo_id}...")
|
| 43 |
+
try:
|
| 44 |
+
downloaded_path = hf_hub_download(repo_id=repo_id, filename=filename, local_dir=CHECKPOINTS_DIR)
|
| 45 |
+
return Path(downloaded_path)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Failed to download: {e}")
|
| 48 |
+
return local_path
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def load_ae(device: str | torch.device = "cuda") -> AutoEncoder:
|
| 52 |
+
print("Init AE")
|
| 53 |
+
ckpt_path = get_checkpoint_path(flux_vae_config.repo_ae, flux_vae_config.repo_id)
|
| 54 |
+
|
| 55 |
+
if ckpt_path.exists():
|
| 56 |
+
print(f"Loading AE checkpoint: {ckpt_path}")
|
| 57 |
+
with torch.device("meta"):
|
| 58 |
+
ae = AutoEncoder(flux_vae_config.ae_params)
|
| 59 |
+
sd = load_sft(str(ckpt_path), device=str(device))
|
| 60 |
+
ae.load_state_dict(sd, strict=False, assign=True)
|
| 61 |
+
else:
|
| 62 |
+
raise RuntimeError("AE Checkpoint is missing! Model cannot train with random VAE.")
|
| 63 |
+
|
| 64 |
+
return ae
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def save_image(x: torch.Tensor, path: str):
|
| 68 |
+
x = x.clamp(-1, 1)
|
| 69 |
+
x = rearrange(x[0], "c h w -> h w c")
|
| 70 |
+
img = Image.fromarray((127.5 * (x + 1.0)).cpu().byte().numpy())
|
| 71 |
+
img.save(path, quality=95, subsampling=0)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def print_load_warning(missing: list[str], unexpected: list[str]):
|
| 75 |
+
if len(missing) > 0 and len(unexpected) > 0:
|
| 76 |
+
print(f"Parameters missing: {len(missing)}. Unexpected: {len(unexpected)}")
|
infer_1step_style.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import math
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import numpy as np
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from torchvision import transforms
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, "/workspace/ChronFLUX/ChronFlux-v1")
|
| 12 |
+
|
| 13 |
+
from flux.utils.util import load_ae
|
| 14 |
+
from flux.chronflux import ChronFlux
|
| 15 |
+
from flux.modules.advanced_encoders import EncoderFactory, DepthEstimator
|
| 16 |
+
from config import get_config
|
| 17 |
+
|
| 18 |
+
PI = math.pi
|
| 19 |
+
|
| 20 |
+
# ===== MODEL =====
|
| 21 |
+
CHECKPOINT = "/workspace/ChronFLUX/ChronFlux-v1/distill_checkpoints/rift_1step_epoch_5.pt"
|
| 22 |
+
|
| 23 |
+
# ===== DATA (FIXED MAPPING) =====
|
| 24 |
+
DATA_PAIRS = [
|
| 25 |
+
{
|
| 26 |
+
"input": "/workspace/ChronFLUX/ChronFlux-v1/EvaluationDS/ChronFLUX-Bench/SDSD/outdoor_png/clear",
|
| 27 |
+
"output": "/workspace/ChronFLUX/ChronFlux-v1/EvaluationDS/DS-BenchFinal/SDSD/outdoor_png/dark_gen",
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"input": "/workspace/ChronFLUX/ChronFlux-v1/EvaluationDS/ChronFLUX-Bench/SDSD/indoor_png/clear",
|
| 31 |
+
"output": "/workspace/ChronFLUX/ChronFlux-v1/EvaluationDS/DS-BenchFinal/SDSD/indoor_png/dark_gen",
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"input": "/workspace/ChronFLUX/ChronFlux-v1/EvaluationDS/ChronFLUX-Bench/SMID/clear",
|
| 35 |
+
"output": "/workspace/ChronFLUX/ChronFlux-v1/EvaluationDS/DS-BenchFinal/SMID/dark_gen",
|
| 36 |
+
},
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
# ===== STYLE =====
|
| 40 |
+
FIXED_STYLE_PATH = "/workspace/ChronFLUX/rafael_ds_ver2/train/night/frame_000002.jpg"
|
| 41 |
+
|
| 42 |
+
# ===== PARAMS =====
|
| 43 |
+
HOURS = list(range(1, 25))
|
| 44 |
+
|
| 45 |
+
IMAGE_SIZE = 384
|
| 46 |
+
DEPTH_SIZE = 48
|
| 47 |
+
DEVICE = "cuda"
|
| 48 |
+
DTYPE = torch.bfloat16
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def load_model(ckpt_path, model_config, device, dtype):
|
| 52 |
+
model = ChronFlux(
|
| 53 |
+
in_channels=16,
|
| 54 |
+
hidden_size=1024,
|
| 55 |
+
num_double_layers=8,
|
| 56 |
+
num_single_layers=16,
|
| 57 |
+
num_heads=16,
|
| 58 |
+
style_dim=512,
|
| 59 |
+
use_moe=True,
|
| 60 |
+
use_depth_attn=True,
|
| 61 |
+
use_adain=True,
|
| 62 |
+
moe_params={"num_experts": model_config.moe.num_experts}
|
| 63 |
+
).to(device).to(dtype)
|
| 64 |
+
|
| 65 |
+
state = torch.load(ckpt_path, map_location="cpu")
|
| 66 |
+
if isinstance(state, dict) and "model_state_dict" in state:
|
| 67 |
+
state = state["model_state_dict"]
|
| 68 |
+
|
| 69 |
+
model.load_state_dict(state, strict=True)
|
| 70 |
+
model.eval().requires_grad_(False)
|
| 71 |
+
|
| 72 |
+
print(f"[INFO] Loaded checkpoint: {ckpt_path}")
|
| 73 |
+
return model
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@torch.no_grad()
|
| 77 |
+
def infer_1step(model, ae, content_lat, depth_small, style_vec, phi, device, dtype):
|
| 78 |
+
t = torch.tensor([0.0], device=device, dtype=dtype)
|
| 79 |
+
z = torch.randn_like(content_lat)
|
| 80 |
+
|
| 81 |
+
v, _ = model(z, content_lat, depth_small, style_vec, t, phi)
|
| 82 |
+
z_final = (z + v * 1.0).clamp(-8, 8)
|
| 83 |
+
|
| 84 |
+
return ae.decode(z_final).clamp(-1, 1)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def save_tensor(tensor, path):
|
| 88 |
+
img = tensor.squeeze(0).cpu().float()
|
| 89 |
+
img = ((img + 1.0) / 2.0).clamp(0, 1)
|
| 90 |
+
img = (img.permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|
| 91 |
+
Image.fromarray(img).save(path)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def main():
|
| 95 |
+
model_config = get_config("q1_paper")
|
| 96 |
+
|
| 97 |
+
print("[INFO] Loading AutoEncoder...")
|
| 98 |
+
ae = load_ae(DEVICE).to(DEVICE).to(DTYPE).eval().requires_grad_(False)
|
| 99 |
+
|
| 100 |
+
print("[INFO] Loading Style Encoder (DINOv2)...")
|
| 101 |
+
style_enc = EncoderFactory.create_encoder(
|
| 102 |
+
"dinov2", DEVICE, 512, False, model_size="base"
|
| 103 |
+
).to(DEVICE).to(DTYPE).eval().requires_grad_(False)
|
| 104 |
+
|
| 105 |
+
print("[INFO] Loading Depth Estimator...")
|
| 106 |
+
depth_est = DepthEstimator(DEVICE)
|
| 107 |
+
|
| 108 |
+
print("[INFO] Loading distilled model...")
|
| 109 |
+
model = load_model(CHECKPOINT, model_config, DEVICE, DTYPE)
|
| 110 |
+
|
| 111 |
+
transform = transforms.Compose([
|
| 112 |
+
transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
|
| 113 |
+
transforms.ToTensor(),
|
| 114 |
+
transforms.Normalize([0.5], [0.5])
|
| 115 |
+
])
|
| 116 |
+
|
| 117 |
+
print("[INFO] Encoding style image...")
|
| 118 |
+
style_t = transform(
|
| 119 |
+
Image.open(FIXED_STYLE_PATH).convert("RGB")
|
| 120 |
+
).unsqueeze(0).to(DEVICE).to(DTYPE)
|
| 121 |
+
|
| 122 |
+
with torch.no_grad():
|
| 123 |
+
style_vec = style_enc(style_t, return_spatial=True)
|
| 124 |
+
|
| 125 |
+
print(f"[INFO] Style vector shape: {style_vec.shape}")
|
| 126 |
+
|
| 127 |
+
# ===== LOOP DATA PAIRS =====
|
| 128 |
+
for pair in DATA_PAIRS:
|
| 129 |
+
content_dir = pair["input"]
|
| 130 |
+
output_dir = pair["output"]
|
| 131 |
+
|
| 132 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 133 |
+
|
| 134 |
+
if not os.path.exists(content_dir):
|
| 135 |
+
print(f"[WARNING] Missing folder: {content_dir}")
|
| 136 |
+
continue
|
| 137 |
+
|
| 138 |
+
content_files = sorted([
|
| 139 |
+
f for f in os.listdir(content_dir)
|
| 140 |
+
if f.lower().endswith((".jpg", ".jpeg", ".png"))
|
| 141 |
+
])
|
| 142 |
+
|
| 143 |
+
if not content_files:
|
| 144 |
+
print(f"[WARNING] No images in: {content_dir}")
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
print(f"\n[DATASET]")
|
| 148 |
+
print(f"[INPUT ] {content_dir}")
|
| 149 |
+
print(f"[OUTPUT] {output_dir}")
|
| 150 |
+
print(f"[INFO ] {len(content_files)} images")
|
| 151 |
+
|
| 152 |
+
for fname in tqdm(content_files, desc=os.path.basename(content_dir)):
|
| 153 |
+
stem = os.path.splitext(fname)[0]
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
content_tensor = transform(
|
| 157 |
+
Image.open(os.path.join(content_dir, fname)).convert("RGB")
|
| 158 |
+
).unsqueeze(0).to(DEVICE).to(DTYPE)
|
| 159 |
+
|
| 160 |
+
with torch.no_grad():
|
| 161 |
+
depth_full = depth_est.predict(
|
| 162 |
+
content_tensor.float(), IMAGE_SIZE
|
| 163 |
+
).to(DTYPE)
|
| 164 |
+
|
| 165 |
+
depth_small = F.interpolate(
|
| 166 |
+
depth_full,
|
| 167 |
+
(DEPTH_SIZE, DEPTH_SIZE),
|
| 168 |
+
mode="nearest"
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
content_lat = ae.encode(content_tensor)
|
| 172 |
+
|
| 173 |
+
# ===== LOOP HOURS =====
|
| 174 |
+
for h in HOURS:
|
| 175 |
+
phi = torch.tensor(
|
| 176 |
+
[h * (2 * PI / 24.0)],
|
| 177 |
+
device=DEVICE,
|
| 178 |
+
dtype=DTYPE
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
out = infer_1step(
|
| 182 |
+
model,
|
| 183 |
+
ae,
|
| 184 |
+
content_lat,
|
| 185 |
+
depth_small,
|
| 186 |
+
style_vec,
|
| 187 |
+
phi,
|
| 188 |
+
DEVICE,
|
| 189 |
+
DTYPE
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
save_path = os.path.join(
|
| 193 |
+
output_dir,
|
| 194 |
+
f"{stem}_h{h:02d}.png"
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
save_tensor(out, save_path)
|
| 198 |
+
|
| 199 |
+
except Exception as e:
|
| 200 |
+
print(f"\n[ERROR] {fname}: {e}")
|
| 201 |
+
import traceback
|
| 202 |
+
traceback.print_exc()
|
| 203 |
+
|
| 204 |
+
print(f"[DONE] {content_dir}: {len(content_files)*len(HOURS)} files")
|
| 205 |
+
|
| 206 |
+
print("\nπ ALL DATASETS DONE")
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
if __name__ == "__main__":
|
| 210 |
+
main()
|
infer_multistep.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import argparse
|
| 4 |
+
import math
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torchvision import transforms
|
| 8 |
+
from torchvision.utils import save_image
|
| 9 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 10 |
+
import numpy as np
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, "/workspace/RIFT-9226")
|
| 14 |
+
|
| 15 |
+
from flux.utils.util import load_ae
|
| 16 |
+
from flux.chronflux import ChronFlux
|
| 17 |
+
from flux.modules.advanced_encoders import EncoderFactory, DepthEstimator
|
| 18 |
+
from config import get_config
|
| 19 |
+
|
| 20 |
+
PI = np.pi
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
# Helpers
|
| 25 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
|
| 27 |
+
def add_label(tensor_img, text):
|
| 28 |
+
img = (tensor_img.clamp(-1, 1) + 1) / 2
|
| 29 |
+
img = img.squeeze(0).permute(1, 2, 0).cpu().float().numpy()
|
| 30 |
+
img = (img * 255).astype(np.uint8)
|
| 31 |
+
pil_img = Image.fromarray(img)
|
| 32 |
+
draw = ImageDraw.Draw(pil_img)
|
| 33 |
+
try:
|
| 34 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 22)
|
| 35 |
+
except Exception:
|
| 36 |
+
font = ImageFont.load_default()
|
| 37 |
+
x, y = 6, 6
|
| 38 |
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 39 |
+
draw.text((x + dx, y + dy), text, fill="black", font=font)
|
| 40 |
+
draw.text((x, y), text, fill="white", font=font)
|
| 41 |
+
out = np.array(pil_img).astype(np.float32) / 255.0
|
| 42 |
+
out = torch.from_numpy(out).permute(2, 0, 1).unsqueeze(0)
|
| 43 |
+
return out * 2 - 1
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_model(checkpoint_path, model_config, device, dtype):
|
| 47 |
+
model = ChronFlux(
|
| 48 |
+
in_channels=16,
|
| 49 |
+
hidden_size=1024,
|
| 50 |
+
num_double_layers=8,
|
| 51 |
+
num_single_layers=16,
|
| 52 |
+
num_heads=16,
|
| 53 |
+
style_dim=512,
|
| 54 |
+
use_moe=True,
|
| 55 |
+
use_depth_attn=True,
|
| 56 |
+
use_adain=True,
|
| 57 |
+
moe_params={"num_experts": model_config.moe.num_experts}
|
| 58 |
+
).to(device).to(dtype)
|
| 59 |
+
|
| 60 |
+
state = torch.load(checkpoint_path, map_location="cpu")
|
| 61 |
+
if "model_state_dict" in state:
|
| 62 |
+
state = state["model_state_dict"]
|
| 63 |
+
model.load_state_dict(state, strict=True)
|
| 64 |
+
model.eval()
|
| 65 |
+
return model
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_cosine_schedule(num_steps):
|
| 69 |
+
return [1.0 - math.cos((i / num_steps) * math.pi / 2.0) for i in range(num_steps)]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 73 |
+
# Core denoising loop (shared by single-seed and multi-seed paths)
|
| 74 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
+
|
| 76 |
+
def denoise(
|
| 77 |
+
model, z_init, content_lat, depth_small, style_vec,
|
| 78 |
+
phi, device, dtype, denoise_steps,
|
| 79 |
+
latent_clamp=20.0, # clamp during ODE integration
|
| 80 |
+
latent_clamp_final=15.0, # tighter clamp right before decode
|
| 81 |
+
):
|
| 82 |
+
"""
|
| 83 |
+
Heun 2nd-order rectified-flow ODE solver.
|
| 84 |
+
|
| 85 |
+
Returns the final latent (NOT yet decoded).
|
| 86 |
+
Caller is responsible for ae.decode(z).
|
| 87 |
+
"""
|
| 88 |
+
t_schedule = get_cosine_schedule(denoise_steps)
|
| 89 |
+
z = z_init.clone()
|
| 90 |
+
|
| 91 |
+
for i in range(denoise_steps):
|
| 92 |
+
t_val = t_schedule[i]
|
| 93 |
+
dt = (t_schedule[i + 1] - t_schedule[i]) if i < denoise_steps - 1 else (1.0 - t_schedule[i])
|
| 94 |
+
|
| 95 |
+
t_cur = torch.tensor([t_val], device=device, dtype=dtype)
|
| 96 |
+
|
| 97 |
+
# ββ Euler predictor βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
v1, _ = model(z, content_lat, depth_small, style_vec, t_cur, phi)
|
| 99 |
+
z_mid = (z + v1 * dt).clamp(-latent_clamp, latent_clamp)
|
| 100 |
+
|
| 101 |
+
# ββ Heun corrector βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
+
t_next_val = t_schedule[i + 1] if i < denoise_steps - 1 else 1.0
|
| 103 |
+
t_next = torch.tensor([t_next_val], device=device, dtype=dtype)
|
| 104 |
+
v2, _ = model(z_mid, content_lat, depth_small, style_vec, t_next, phi)
|
| 105 |
+
|
| 106 |
+
z = z + (v1 + v2) * 0.5 * dt
|
| 107 |
+
z = z.clamp(-latent_clamp, latent_clamp) # keep ODE stable
|
| 108 |
+
|
| 109 |
+
# ββ Final pre-decode clamp βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 110 |
+
z = z.clamp(-latent_clamp_final, latent_clamp_final)
|
| 111 |
+
return z
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
+
# Per-image inference
|
| 116 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 117 |
+
|
| 118 |
+
def run_inference(
|
| 119 |
+
model, ae, style_enc, depth_est,
|
| 120 |
+
content_path, style_path,
|
| 121 |
+
device, dtype,
|
| 122 |
+
denoise_steps, image_size, depth_size,
|
| 123 |
+
noise_alpha=0.92, # [DEPRECATED] kept for signature compatibility
|
| 124 |
+
num_seeds=1, # >1 β average over multiple trajectories
|
| 125 |
+
hours=None, # list of hours to render; default = 0..24
|
| 126 |
+
):
|
| 127 |
+
"""
|
| 128 |
+
Returns a strip tensor:
|
| 129 |
+
[content | style | depth | hour_0 | hour_1 | ... | hour_24]
|
| 130 |
+
"""
|
| 131 |
+
if hours is None:
|
| 132 |
+
hours = list(range(25)) # 0 h β¦ 24 h
|
| 133 |
+
|
| 134 |
+
transform = transforms.Compose([
|
| 135 |
+
transforms.Resize((image_size, image_size)),
|
| 136 |
+
transforms.ToTensor(),
|
| 137 |
+
transforms.Normalize([0.5], [0.5])
|
| 138 |
+
])
|
| 139 |
+
|
| 140 |
+
content_tensor = transform(Image.open(content_path).convert("RGB")).unsqueeze(0).to(device).to(dtype)
|
| 141 |
+
style_tensor = transform(Image.open(style_path).convert("RGB")).unsqueeze(0).to(device).to(dtype)
|
| 142 |
+
|
| 143 |
+
with torch.no_grad():
|
| 144 |
+
# ββ Encode once βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 145 |
+
depth_full = depth_est.predict(content_tensor.float(), image_size).to(dtype)
|
| 146 |
+
depth_small = F.interpolate(depth_full, size=(depth_size, depth_size), mode="nearest")
|
| 147 |
+
|
| 148 |
+
content_lat = ae.encode(content_tensor)
|
| 149 |
+
|
| 150 |
+
style_vec = style_enc(style_tensor, return_spatial=True)
|
| 151 |
+
|
| 152 |
+
depth_vis = (depth_full.repeat(1, 3, 1, 1) - 0.5) / 0.5
|
| 153 |
+
|
| 154 |
+
generated = []
|
| 155 |
+
for h in hours:
|
| 156 |
+
phi_val = h * (2 * PI / 24.0)
|
| 157 |
+
phi = torch.tensor([phi_val], device=device, dtype=dtype)
|
| 158 |
+
|
| 159 |
+
if num_seeds == 1:
|
| 160 |
+
# ββ Single trajectory ββββββββββββββββββββββββββββββββββββββββ
|
| 161 |
+
z_init = torch.randn_like(content_lat)
|
| 162 |
+
|
| 163 |
+
z_final = denoise(
|
| 164 |
+
model, z_init, content_lat, depth_small, style_vec,
|
| 165 |
+
phi, device, dtype, denoise_steps
|
| 166 |
+
)
|
| 167 |
+
out = ae.decode(z_final).clamp(-1, 1)
|
| 168 |
+
|
| 169 |
+
else:
|
| 170 |
+
decoded_sum = torch.zeros(1, 3, image_size, image_size, device=device, dtype=torch.float32)
|
| 171 |
+
for _ in range(num_seeds):
|
| 172 |
+
z_init = torch.randn_like(content_lat)
|
| 173 |
+
|
| 174 |
+
z_final = denoise(
|
| 175 |
+
model, z_init, content_lat, depth_small, style_vec,
|
| 176 |
+
phi, device, dtype, denoise_steps
|
| 177 |
+
)
|
| 178 |
+
decoded_sum += ae.decode(z_final).clamp(-1, 1).float()
|
| 179 |
+
out = (decoded_sum / num_seeds).clamp(-1, 1)
|
| 180 |
+
|
| 181 |
+
generated.append(add_label(out.float(), f"{h}h").to(device))
|
| 182 |
+
|
| 183 |
+
content_labeled = add_label(content_tensor.float(), "Content").to(device)
|
| 184 |
+
style_labeled = add_label(style_tensor.float(), "Style").to(device)
|
| 185 |
+
depth_labeled = add_label(depth_vis.float(), "Depth").to(device)
|
| 186 |
+
|
| 187 |
+
strip = torch.cat([content_labeled, style_labeled, depth_labeled] + generated, dim=3)
|
| 188 |
+
return strip
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
+
# CLI entry-point
|
| 193 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 194 |
+
|
| 195 |
+
def main():
|
| 196 |
+
parser = argparse.ArgumentParser(description="ChronFlux 24-hour inference")
|
| 197 |
+
parser.add_argument("--checkpoint", type=str, default="/workspace/RIFT-9226/checkpoints/rift_epoch_30.pt")
|
| 198 |
+
parser.add_argument("--content_dir", type=str, default="/workspace/RIFT/rafael_ds_ver2/img_test/cnt")
|
| 199 |
+
parser.add_argument("--style_dir", type=str, default="/workspace/RIFT/rafael_ds_ver2/img_test/sty")
|
| 200 |
+
parser.add_argument("--output_dir", type=str, default="/workspace/RIFT-9226/inference_outputs")
|
| 201 |
+
parser.add_argument("--denoise_steps", type=int, default=50,
|
| 202 |
+
help="More steps = better quality. 50 is a good default; try 75 for maximum quality.")
|
| 203 |
+
parser.add_argument("--image_size", type=int, default=384)
|
| 204 |
+
parser.add_argument("--depth_size", type=int, default=48)
|
| 205 |
+
parser.add_argument("--noise_alpha", type=float, default=0.85,
|
| 206 |
+
help="[DEPRECATED] No longer used. Model requires pure noise init.")
|
| 207 |
+
parser.add_argument("--num_seeds", type=int, default=1,
|
| 208 |
+
help="Average over N random seeds (slow but reduces noise artifacts).")
|
| 209 |
+
parser.add_argument("--device", type=str, default="cuda")
|
| 210 |
+
parser.add_argument("--dtype", type=str, default="bfloat16")
|
| 211 |
+
args = parser.parse_args()
|
| 212 |
+
|
| 213 |
+
device = args.device
|
| 214 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float32
|
| 215 |
+
|
| 216 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 217 |
+
|
| 218 |
+
print(f"[INFO] Loading config...")
|
| 219 |
+
model_config = get_config("q1_paper")
|
| 220 |
+
|
| 221 |
+
print(f"[INFO] Loading AutoEncoder...")
|
| 222 |
+
ae = load_ae(device).to(device).to(dtype).eval().requires_grad_(False)
|
| 223 |
+
|
| 224 |
+
print(f"[INFO] Loading Style Encoder (DINOv2)...")
|
| 225 |
+
style_enc = EncoderFactory.create_encoder(
|
| 226 |
+
"dinov2", device, 512, False, model_size="base"
|
| 227 |
+
).to(device).to(dtype).eval().requires_grad_(False)
|
| 228 |
+
|
| 229 |
+
print(f"[INFO] Loading Depth Estimator...")
|
| 230 |
+
depth_est = DepthEstimator(device)
|
| 231 |
+
|
| 232 |
+
print(f"[INFO] Loading ChronFlux from {args.checkpoint}...")
|
| 233 |
+
model = load_model(args.checkpoint, model_config, device, dtype)
|
| 234 |
+
model.requires_grad_(False)
|
| 235 |
+
|
| 236 |
+
print(f"[INFO] denoise_steps={args.denoise_steps} | num_seeds={args.num_seeds} | Pure Noise Init: ENABLED")
|
| 237 |
+
|
| 238 |
+
content_files = sorted([
|
| 239 |
+
f for f in os.listdir(args.content_dir)
|
| 240 |
+
if f.lower().endswith((".jpg", ".jpeg", ".png"))
|
| 241 |
+
])
|
| 242 |
+
style_files = sorted([
|
| 243 |
+
f for f in os.listdir(args.style_dir)
|
| 244 |
+
if f.lower().endswith((".jpg", ".jpeg", ".png"))
|
| 245 |
+
])
|
| 246 |
+
|
| 247 |
+
if not content_files:
|
| 248 |
+
print("[ERROR] No content images found.")
|
| 249 |
+
return
|
| 250 |
+
if not style_files:
|
| 251 |
+
print("[ERROR] No style images found.")
|
| 252 |
+
return
|
| 253 |
+
|
| 254 |
+
print(f"[INFO] Found {len(content_files)} content images and {len(style_files)} style images.")
|
| 255 |
+
|
| 256 |
+
for content_file in tqdm(content_files, desc="Content images"):
|
| 257 |
+
content_path = os.path.join(args.content_dir, content_file)
|
| 258 |
+
content_stem = os.path.splitext(content_file)[0]
|
| 259 |
+
|
| 260 |
+
for style_file in style_files:
|
| 261 |
+
style_path = os.path.join(args.style_dir, style_file)
|
| 262 |
+
style_stem = os.path.splitext(style_file)[0]
|
| 263 |
+
|
| 264 |
+
out_name = f"{content_stem}__sty_{style_stem}.png"
|
| 265 |
+
out_path = os.path.join(args.output_dir, out_name)
|
| 266 |
+
|
| 267 |
+
if os.path.exists(out_path):
|
| 268 |
+
print(f"[SKIP] {out_name} already exists.")
|
| 269 |
+
continue
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
strip = run_inference(
|
| 273 |
+
model=model,
|
| 274 |
+
ae=ae,
|
| 275 |
+
style_enc=style_enc,
|
| 276 |
+
depth_est=depth_est,
|
| 277 |
+
content_path=content_path,
|
| 278 |
+
style_path=style_path,
|
| 279 |
+
device=device,
|
| 280 |
+
dtype=dtype,
|
| 281 |
+
denoise_steps=args.denoise_steps,
|
| 282 |
+
image_size=args.image_size,
|
| 283 |
+
depth_size=args.depth_size,
|
| 284 |
+
noise_alpha=args.noise_alpha,
|
| 285 |
+
num_seeds=args.num_seeds,
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
save_image(strip, out_path, normalize=True, value_range=(-1, 1))
|
| 289 |
+
print(f"[SAVED] {out_path}")
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
print(f"[ERROR] Failed on {content_file} x {style_file}: {e}")
|
| 293 |
+
import traceback
|
| 294 |
+
traceback.print_exc()
|
| 295 |
+
|
| 296 |
+
print(f"\n[DONE] All outputs saved to: {args.output_dir}")
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
if __name__ == "__main__":
|
| 300 |
+
main()
|
infer_multistep_vid.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import argparse
|
| 4 |
+
import math
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
from PIL import Image
|
| 11 |
+
from torchvision import transforms
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from moviepy import VideoFileClip
|
| 15 |
+
MOVIEPY_AVAILABLE = True
|
| 16 |
+
except ImportError:
|
| 17 |
+
MOVIEPY_AVAILABLE = False
|
| 18 |
+
|
| 19 |
+
sys.path.insert(0, "/workspace/ChronFLUX")
|
| 20 |
+
|
| 21 |
+
from flux.utils.util import load_ae
|
| 22 |
+
from flux.chronflux import ChronFlux
|
| 23 |
+
from flux.modules.advanced_encoders import EncoderFactory, DepthEstimator
|
| 24 |
+
from config import get_config
|
| 25 |
+
|
| 26 |
+
PI = math.pi
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_model(checkpoint_path, model_config, device, dtype):
|
| 30 |
+
model = ChronFlux(
|
| 31 |
+
in_channels=16,
|
| 32 |
+
hidden_size=1024,
|
| 33 |
+
num_double_layers=8,
|
| 34 |
+
num_single_layers=16,
|
| 35 |
+
num_heads=16,
|
| 36 |
+
style_dim=512,
|
| 37 |
+
use_moe=True,
|
| 38 |
+
use_depth_attn=True,
|
| 39 |
+
use_adain=True,
|
| 40 |
+
moe_params={"num_experts": model_config.moe.num_experts}
|
| 41 |
+
).to(device).to(dtype)
|
| 42 |
+
|
| 43 |
+
state = torch.load(checkpoint_path, map_location="cpu")
|
| 44 |
+
if "model_state_dict" in state:
|
| 45 |
+
state = state["model_state_dict"]
|
| 46 |
+
model.load_state_dict(state, strict=True)
|
| 47 |
+
model.eval()
|
| 48 |
+
model.requires_grad_(False)
|
| 49 |
+
return model
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_cosine_schedule(num_steps):
|
| 53 |
+
ts = []
|
| 54 |
+
for i in range(num_steps):
|
| 55 |
+
t_val = 1.0 - math.cos((i / num_steps) * math.pi / 2.0)
|
| 56 |
+
ts.append(t_val)
|
| 57 |
+
return ts
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def get_safe_resolution(width, height, max_height=720):
|
| 61 |
+
if height <= max_height and width % 16 == 0 and height % 16 == 0:
|
| 62 |
+
return width, height
|
| 63 |
+
ratio = max_height / height
|
| 64 |
+
new_width = int(width * ratio)
|
| 65 |
+
new_height = max_height
|
| 66 |
+
if new_width % 16 != 0:
|
| 67 |
+
new_width = new_width - (new_width % 16)
|
| 68 |
+
if new_height % 16 != 0:
|
| 69 |
+
new_height = new_height - (new_height % 16)
|
| 70 |
+
return new_width, new_height
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def tensor_to_numpy_bgr(tensor):
|
| 74 |
+
img = tensor.clamp(-1, 1)
|
| 75 |
+
img = (img + 1.0) / 2.0
|
| 76 |
+
img = img.squeeze(0).permute(1, 2, 0).cpu().float().numpy()
|
| 77 |
+
img = (img * 255).astype(np.uint8)
|
| 78 |
+
img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
| 79 |
+
return img_bgr
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def inference_single_frame(
|
| 83 |
+
model, ae, depth_est,
|
| 84 |
+
frame_tensor,
|
| 85 |
+
style_vec_cached,
|
| 86 |
+
phi, device, dtype,
|
| 87 |
+
denoise_steps, depth_size,
|
| 88 |
+
base_noise
|
| 89 |
+
):
|
| 90 |
+
with torch.no_grad():
|
| 91 |
+
image_size = frame_tensor.shape[-1]
|
| 92 |
+
|
| 93 |
+
depth_full = depth_est.predict(frame_tensor.float(), image_size).to(dtype)
|
| 94 |
+
depth_small = F.interpolate(depth_full, size=(depth_size, depth_size), mode="nearest")
|
| 95 |
+
|
| 96 |
+
# content_gray = transforms.functional.rgb_to_grayscale(frame_tensor, num_output_channels=3)
|
| 97 |
+
# content_lat = ae.encode(content_gray)
|
| 98 |
+
content_lat = ae.encode(frame_tensor)
|
| 99 |
+
|
| 100 |
+
style_vec = style_vec_cached
|
| 101 |
+
|
| 102 |
+
t_schedule = get_cosine_schedule(denoise_steps)
|
| 103 |
+
|
| 104 |
+
z = base_noise.clone()
|
| 105 |
+
|
| 106 |
+
for i in range(denoise_steps):
|
| 107 |
+
t_val = t_schedule[i]
|
| 108 |
+
if i < denoise_steps - 1:
|
| 109 |
+
dt = t_schedule[i + 1] - t_schedule[i]
|
| 110 |
+
else:
|
| 111 |
+
dt = 1.0 - t_schedule[i]
|
| 112 |
+
|
| 113 |
+
t = torch.tensor([t_val], device=device, dtype=dtype)
|
| 114 |
+
|
| 115 |
+
v1, _ = model(z, content_lat, depth_small, style_vec, t, phi)
|
| 116 |
+
z_mid = (z + v1 * dt).clamp(-8, 8)
|
| 117 |
+
|
| 118 |
+
if i < denoise_steps - 1:
|
| 119 |
+
t_next_val = t_schedule[i + 1]
|
| 120 |
+
else:
|
| 121 |
+
t_next_val = 1.0
|
| 122 |
+
t_next = torch.tensor([t_next_val], device=device, dtype=dtype)
|
| 123 |
+
v2, _ = model(z_mid, content_lat, depth_small, style_vec, t_next, phi)
|
| 124 |
+
|
| 125 |
+
z = z + (v1 + v2) * 0.5 * dt
|
| 126 |
+
z = z.clamp(-8, 8)
|
| 127 |
+
|
| 128 |
+
z = z.clamp(-6, 6)
|
| 129 |
+
out = ae.decode(z).clamp(-1, 1)
|
| 130 |
+
return out
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def main():
|
| 134 |
+
parser = argparse.ArgumentParser(description="ChronFlux Video Inference - Day/Night Cycle (Fast)")
|
| 135 |
+
parser.add_argument("--checkpoint", type=str, required=True)
|
| 136 |
+
parser.add_argument("--video_path", type=str, required=True)
|
| 137 |
+
parser.add_argument("--style_path", type=str, required=True, help="αΊ’nh style ban ΔΓͺm")
|
| 138 |
+
parser.add_argument("--output_dir", type=str, default="/workspace/ChronFLUX/video_outputs")
|
| 139 |
+
parser.add_argument("--denoise_steps", type=int, default=50)
|
| 140 |
+
parser.add_argument("--image_size", type=int, default=384)
|
| 141 |
+
parser.add_argument("--depth_size", type=int, default=48)
|
| 142 |
+
parser.add_argument("--max_height", type=int, default=384)
|
| 143 |
+
parser.add_argument("--hour_start", type=float, default=0.0)
|
| 144 |
+
parser.add_argument("--hour_end", type=float, default=24.0)
|
| 145 |
+
parser.add_argument("--cycle", action="store_true")
|
| 146 |
+
parser.add_argument("--no_audio", action="store_true")
|
| 147 |
+
parser.add_argument("--device", type=str, default="cuda")
|
| 148 |
+
parser.add_argument("--dtype", type=str, default="bfloat16")
|
| 149 |
+
args = parser.parse_args()
|
| 150 |
+
|
| 151 |
+
device = args.device
|
| 152 |
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float32
|
| 153 |
+
|
| 154 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 155 |
+
|
| 156 |
+
print("[INFO] Loading config...")
|
| 157 |
+
model_config = get_config("q1_paper")
|
| 158 |
+
|
| 159 |
+
print("[INFO] Loading AutoEncoder...")
|
| 160 |
+
ae = load_ae(device).to(device).to(dtype).eval().requires_grad_(False)
|
| 161 |
+
|
| 162 |
+
print("[INFO] Loading Style Encoder (DINOv2)...")
|
| 163 |
+
style_enc = EncoderFactory.create_encoder(
|
| 164 |
+
"dinov2", device, 512, False, model_size="base"
|
| 165 |
+
).to(device).to(dtype).eval().requires_grad_(False)
|
| 166 |
+
|
| 167 |
+
print("[INFO] Loading Depth Estimator...")
|
| 168 |
+
depth_est = DepthEstimator(device)
|
| 169 |
+
|
| 170 |
+
print(f"[INFO] Loading ChronFlux from {args.checkpoint}...")
|
| 171 |
+
model = load_model(args.checkpoint, model_config, device, dtype)
|
| 172 |
+
|
| 173 |
+
transform = transforms.Compose([
|
| 174 |
+
transforms.Resize((args.image_size, args.image_size)),
|
| 175 |
+
transforms.ToTensor(),
|
| 176 |
+
transforms.Normalize([0.5], [0.5])
|
| 177 |
+
])
|
| 178 |
+
|
| 179 |
+
print("[INFO] Loading & encoding style image (cached for all frames)...")
|
| 180 |
+
style_pil = Image.open(args.style_path).convert("RGB")
|
| 181 |
+
style_tensor = transform(style_pil).unsqueeze(0).to(device).to(dtype)
|
| 182 |
+
with torch.no_grad():
|
| 183 |
+
style_vec_cached = style_enc(style_tensor, return_spatial=True)
|
| 184 |
+
print(f"[INFO] Style vector cached: {style_vec_cached.shape}")
|
| 185 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 186 |
+
|
| 187 |
+
print("[INFO] Opening video...")
|
| 188 |
+
cap = cv2.VideoCapture(args.video_path)
|
| 189 |
+
if not cap.isOpened():
|
| 190 |
+
raise IOError(f"KhΓ΄ng thα» mα» video: {args.video_path}")
|
| 191 |
+
|
| 192 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 193 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 194 |
+
orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 195 |
+
orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 196 |
+
|
| 197 |
+
proc_w, proc_h = get_safe_resolution(orig_w, orig_h, args.max_height)
|
| 198 |
+
print(f"[INFO] Video: {orig_w}x{orig_h} β resize {proc_w}x{proc_h}, {fps:.2f} FPS, {total_frames} frames")
|
| 199 |
+
print(f"[INFO] Hour range: {args.hour_start}h β {args.hour_end}h, denoise_steps={args.denoise_steps} (Heun)")
|
| 200 |
+
print(f"[INFO] Speedup: style_enc skipped for {total_frames - 1} frames")
|
| 201 |
+
|
| 202 |
+
video_name = os.path.splitext(os.path.basename(args.video_path))[0]
|
| 203 |
+
temp_path = os.path.join(args.output_dir, f"{video_name}_temp.mp4")
|
| 204 |
+
output_path = os.path.join(args.output_dir, f"{video_name}_chronflux.mp4")
|
| 205 |
+
|
| 206 |
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 207 |
+
writer = cv2.VideoWriter(temp_path, fourcc, fps, (proc_w, proc_h))
|
| 208 |
+
|
| 209 |
+
frame_idx = 0
|
| 210 |
+
pbar = tqdm(total=total_frames, desc="Processing frames")
|
| 211 |
+
|
| 212 |
+
base_noise = None
|
| 213 |
+
|
| 214 |
+
while True:
|
| 215 |
+
ret, frame_bgr = cap.read()
|
| 216 |
+
if not ret:
|
| 217 |
+
break
|
| 218 |
+
|
| 219 |
+
frame_resized = cv2.resize(frame_bgr, (proc_w, proc_h), interpolation=cv2.INTER_AREA)
|
| 220 |
+
frame_rgb = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB)
|
| 221 |
+
frame_pil = Image.fromarray(frame_rgb)
|
| 222 |
+
frame_tensor_inf = transform(frame_pil).unsqueeze(0).to(device).to(dtype)
|
| 223 |
+
|
| 224 |
+
if base_noise is None:
|
| 225 |
+
with torch.no_grad():
|
| 226 |
+
content_gray = transforms.functional.rgb_to_grayscale(frame_tensor_inf, num_output_channels=3)
|
| 227 |
+
first_lat = ae.encode(content_gray)
|
| 228 |
+
base_noise = torch.randn_like(first_lat)
|
| 229 |
+
print(f"[INFO] Base noise initialized: {base_noise.shape}")
|
| 230 |
+
|
| 231 |
+
if args.cycle:
|
| 232 |
+
progress = (frame_idx % total_frames) / max(total_frames - 1, 1)
|
| 233 |
+
if (frame_idx // total_frames) % 2 == 0:
|
| 234 |
+
hour = args.hour_start + progress * (args.hour_end - args.hour_start)
|
| 235 |
+
else:
|
| 236 |
+
hour = args.hour_end - progress * (args.hour_end - args.hour_start)
|
| 237 |
+
else:
|
| 238 |
+
progress = frame_idx / max(total_frames - 1, 1)
|
| 239 |
+
hour = args.hour_start + progress * (args.hour_end - args.hour_start)
|
| 240 |
+
|
| 241 |
+
phi_val = hour * (2 * PI / 24.0)
|
| 242 |
+
phi = torch.tensor([phi_val], device=device, dtype=dtype)
|
| 243 |
+
|
| 244 |
+
out = inference_single_frame(
|
| 245 |
+
model=model,
|
| 246 |
+
ae=ae,
|
| 247 |
+
depth_est=depth_est,
|
| 248 |
+
frame_tensor=frame_tensor_inf,
|
| 249 |
+
style_vec_cached=style_vec_cached,
|
| 250 |
+
phi=phi,
|
| 251 |
+
device=device,
|
| 252 |
+
dtype=dtype,
|
| 253 |
+
denoise_steps=args.denoise_steps,
|
| 254 |
+
depth_size=args.depth_size,
|
| 255 |
+
base_noise=base_noise
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
out_bgr = tensor_to_numpy_bgr(out)
|
| 259 |
+
out_bgr_resized = cv2.resize(out_bgr, (proc_w, proc_h), interpolation=cv2.INTER_LINEAR)
|
| 260 |
+
writer.write(out_bgr_resized)
|
| 261 |
+
|
| 262 |
+
frame_idx += 1
|
| 263 |
+
pbar.update(1)
|
| 264 |
+
pbar.set_postfix({"hour": f"{hour:.1f}h", "phi": f"{phi_val:.3f}"})
|
| 265 |
+
|
| 266 |
+
cap.release()
|
| 267 |
+
writer.release()
|
| 268 |
+
pbar.close()
|
| 269 |
+
print(f"[INFO] Finished processing {frame_idx} frames.")
|
| 270 |
+
|
| 271 |
+
if not args.no_audio and MOVIEPY_AVAILABLE:
|
| 272 |
+
print("[INFO] Adding audio from original video...")
|
| 273 |
+
try:
|
| 274 |
+
original_clip = VideoFileClip(args.video_path)
|
| 275 |
+
generated_clip = VideoFileClip(temp_path)
|
| 276 |
+
if original_clip.audio is not None:
|
| 277 |
+
final_clip = generated_clip.set_audio(original_clip.audio)
|
| 278 |
+
final_clip.write_videofile(output_path, codec="libx264", audio_codec="aac", logger="bar")
|
| 279 |
+
original_clip.close()
|
| 280 |
+
generated_clip.close()
|
| 281 |
+
os.remove(temp_path)
|
| 282 |
+
print("[INFO] Audio added successfully.")
|
| 283 |
+
else:
|
| 284 |
+
original_clip.close()
|
| 285 |
+
generated_clip.close()
|
| 286 |
+
os.rename(temp_path, output_path)
|
| 287 |
+
print("[INFO] No audio track found in original video.")
|
| 288 |
+
except Exception as e:
|
| 289 |
+
print(f"[WARN] Could not add audio: {e}")
|
| 290 |
+
os.rename(temp_path, output_path)
|
| 291 |
+
else:
|
| 292 |
+
if not MOVIEPY_AVAILABLE and not args.no_audio:
|
| 293 |
+
print("[WARN] moviepy not installed, skipping audio.")
|
| 294 |
+
os.rename(temp_path, output_path)
|
| 295 |
+
|
| 296 |
+
print(f"\n[DONE] Output saved to: {output_path}")
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
if __name__ == "__main__":
|
| 300 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.6.0
|
| 2 |
+
torchvision>=0.19.0
|
| 3 |
+
numpy>=1.24.0
|
| 4 |
+
pillow>=10.3.0
|
| 5 |
+
tqdm
|
| 6 |
+
einops
|
| 7 |
+
safetensors
|
| 8 |
+
transformers>=4.48.0
|
| 9 |
+
accelerate
|
| 10 |
+
opencv-python
|
| 11 |
+
sentencepiece
|
| 12 |
+
huggingface_hub
|
| 13 |
+
timm>=0.9.0
|
| 14 |
+
lpips
|
| 15 |
+
flash-attn>=2.0.0
|