File size: 15,257 Bytes
6786303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccbbabe
 
 
6786303
 
ccbbabe
 
 
 
 
 
 
 
6786303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccbbabe
6786303
ccbbabe
 
 
 
6786303
 
 
 
 
 
 
ccbbabe
 
 
6786303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccbbabe
 
 
 
 
 
 
 
6786303
 
ccbbabe
 
 
 
 
6786303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccbbabe
 
 
 
6786303
 
 
 
 
 
 
 
 
 
 
ccbbabe
6786303
ccbbabe
6786303
ccbbabe
6786303
ccbbabe
6786303
ccbbabe
6786303
ccbbabe
6786303
 
ccbbabe
 
6786303
ccbbabe
6786303
ccbbabe
 
6786303
ccbbabe
6786303
ccbbabe
 
6786303
ccbbabe
6786303
ccbbabe
 
 
 
6786303
ccbbabe
 
6786303
 
ccbbabe
 
6786303
 
ccbbabe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6786303
 
 
 
 
 
 
 
 
ccbbabe
 
6786303
 
 
 
 
 
 
 
ccbbabe
 
 
 
 
 
 
 
 
 
 
6786303
ccbbabe
 
 
 
 
 
6786303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccbbabe
 
 
 
 
6786303
ccbbabe
 
6786303
ccbbabe
 
 
 
 
6786303
ccbbabe
6786303
ccbbabe
 
 
6786303
 
 
 
 
 
 
 
ccbbabe
 
 
 
 
6786303
 
 
 
 
 
 
 
 
 
 
ccbbabe
6786303
 
ccbbabe
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# Copyright 2026 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Hub custom pipeline: JiTPipeline for FD-Loss post-trained JiT (legacy time, velocity Euler)."""

from __future__ import annotations

import importlib.util
import inspect
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union

import torch
from diffusers.pipelines.pipeline_utils import DiffusionPipeline, ImagePipelineOutput
from diffusers.utils.torch_utils import randn_tensor

EXAMPLE_DOC_STRING = """
    Examples:
        ```py
        >>> from pathlib import Path
        >>> import torch
        >>> from diffusers import DiffusionPipeline

        >>> model_dir = Path("BiliSakura/FD-Loss-diffusers/JiT-B-16-SIM")
        >>> pipe = DiffusionPipeline.from_pretrained(
        ...     str(model_dir),
        ...     custom_pipeline=str(model_dir / "pipeline.py"),
        ...     trust_remote_code=True,
        ... )
        >>> pipe.to("cuda")
        >>> image = pipe(class_labels="golden retriever", num_inference_steps=1, guidance_scale=3.0).images[0]
        ```
"""

RECOMMENDED_NOISE_BY_SIZE = {256: 1.0, 512: 2.0}


class JiTPipeline(DiffusionPipeline):
    r"""
    Class-conditional JiT pipeline for FD-Loss checkpoints (1-NFE velocity Euler, legacy time convention).

    Args:
        transformer ([`JiTTransformer2DModel`]): JiT backbone.
        scheduler ([`FDLossFlowMatchScheduler`], *optional*): Bundled flow-matching scheduler (`t=1→0`).
            Accepts `generator` in `step()` for reproducible stochastic sampling.
        id2label (`dict[int, str]`, *optional*): ImageNet id → label strings from `model_index.json`.
    """

    model_cpu_offload_seq = "transformer"
    _optional_components = ["scheduler"]

    @staticmethod
    def prepare_extra_step_kwargs(
        scheduler,
        generator=None,
        eta: float | None = None,
    ):
        kwargs = {}
        step_params = set(inspect.signature(scheduler.step).parameters.keys())
        if "generator" in step_params:
            kwargs["generator"] = generator
        if eta is not None and "eta" in step_params:
            kwargs["eta"] = eta
        return kwargs

    @staticmethod
    def _resolve_inference_generator(
        device: Union[str, torch.device],
        generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
    ) -> Optional[Union[torch.Generator, List[torch.Generator]]]:
        if generator is None:
            return None
        if isinstance(device, str):
            device = torch.device(device)
        device_type = device.type

        def _relocate(gen: torch.Generator) -> torch.Generator:
            if gen.device.type == device_type:
                return gen
            return torch.Generator(device=device_type).manual_seed(gen.initial_seed())

        if isinstance(generator, list):
            return [_relocate(g) for g in generator]
        return _relocate(generator)

    @staticmethod
    def _coerce_scheduler(scheduler, transformer):
        """Always load the bundled FD-Loss flow scheduler (ignore model_index diffusers schedulers)."""
        variant_path = getattr(transformer.config, "_name_or_path", None)
        if variant_path:
            scheduler_dir = Path(variant_path).resolve().parent / "scheduler"
            module_path = scheduler_dir / "scheduling_flow_match_fd.py"
            config_path = scheduler_dir / "scheduler_config.json"
            if module_path.is_file() and config_path.is_file():
                spec = importlib.util.spec_from_file_location("scheduling_flow_match_fd", module_path)
                if spec is not None and spec.loader is not None:
                    module = importlib.util.module_from_spec(spec)
                    spec.loader.exec_module(module)
                    scheduler_cls = getattr(module, "FDLossFlowMatchScheduler")
                    return scheduler_cls.from_pretrained(str(scheduler_dir))
        scheduler_dir = Path(__file__).resolve().parent / "scheduler"
        module_path = scheduler_dir / "scheduling_flow_match_fd.py"
        config_path = scheduler_dir / "scheduler_config.json"
        if module_path.is_file() and config_path.is_file():
            spec = importlib.util.spec_from_file_location("scheduling_flow_match_fd", module_path)
            if spec is not None and spec.loader is not None:
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                scheduler_cls = getattr(module, "FDLossFlowMatchScheduler")
                return scheduler_cls.from_pretrained(str(scheduler_dir))
        raise ValueError("FD-Loss JiT requires a bundled FDLossFlowMatchScheduler under `scheduler/`.")

    def __init__(
        self,
        transformer,
        scheduler=None,
        id2label: Optional[Dict[Union[int, str], str]] = None,
    ):
        super().__init__()
        self.legacy_time_convention = True
        scheduler = self._coerce_scheduler(scheduler, transformer)
        self.register_modules(transformer=transformer, scheduler=scheduler)
        self._id2label = self._normalize_id2label(id2label)
        self.labels = self._build_label2id(self._id2label)
        self._labels_loaded_from_model_index = bool(self._id2label)


    @property
    def id2label(self) -> Dict[int, str]:
        self._ensure_labels_loaded()
        return self._id2label

    def get_label_ids(self, label: Union[str, List[str]]) -> List[int]:
        r"""Map ImageNet English label strings to class ids."""
        self._ensure_labels_loaded()
        if isinstance(label, str):
            label = [label]
        missing = [item for item in label if item not in self.labels]
        if missing:
            preview = ", ".join(list(self.labels.keys())[:8])
            raise ValueError(f"Unknown label(s): {missing}. Examples: {preview}, ...")
        return [self.labels[item] for item in label]

    def _backbone_timestep(self, t: torch.Tensor) -> torch.Tensor:
        if self.legacy_time_convention:
            return 1.0 - t
        return t

    def _cfg_interval(self, interval_min: float, interval_max: float, device: torch.device) -> Tuple[float, float]:
        low = float(self._backbone_timestep(torch.tensor(interval_min, device=device)))
        high = float(self._backbone_timestep(torch.tensor(interval_max, device=device)))
        return min(low, high), max(low, high)

    def _flow_time_tensors(self, t: torch.Tensor, sample: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """Broadcast scalar flow time and per-sample backbone timesteps for `sample` (B, C, H, W)."""
        flow_t = t.reshape(()).to(device=sample.device, dtype=sample.dtype)
        t_view = flow_t.reshape(*([1] * sample.ndim))
        t_bb = self._backbone_timestep(flow_t).expand(sample.shape[0])
        return t_view, t_bb

    def _predict_velocity(
        self,
        sample: torch.Tensor,
        t: torch.Tensor,
        class_labels: torch.Tensor,
        guidance_scale: float,
        cfg_interval: Tuple[float, float],
        t_eps: float,
        interpolate_pos_encoding: bool,
    ) -> torch.Tensor:
        t_view, t_bb = self._flow_time_tensors(t, sample)
        num_classes = int(getattr(self.transformer.config, "num_classes", 1000))

        x_cond = self.transformer(
            sample,
            timestep=t_bb,
            class_labels=class_labels,
            interpolate_pos_encoding=interpolate_pos_encoding,
        ).sample
        v_cond = (sample - x_cond) / t_view.clamp_min(t_eps)

        if guidance_scale <= 1.0:
            return v_cond

        class_null = torch.full_like(class_labels, num_classes)
        x_uncond = self.transformer(
            sample,
            timestep=t_bb,
            class_labels=class_null,
            interpolate_pos_encoding=interpolate_pos_encoding,
        ).sample
        v_uncond = (sample - x_uncond) / t_view.clamp_min(t_eps)

        low, high = cfg_interval
        flow_t = t.reshape(()).to(device=sample.device, dtype=sample.dtype)
        mask = (flow_t < high) & ((low == 0) | (flow_t > low))
        scale = torch.where(
            mask,
            torch.tensor(guidance_scale, device=sample.device, dtype=sample.dtype),
            torch.tensor(1.0, device=sample.device, dtype=sample.dtype),
        )
        while scale.ndim < v_cond.ndim:
            scale = scale.unsqueeze(-1)
        return v_uncond + scale * (v_cond - v_uncond)

    @staticmethod
    def _normalize_id2label(id2label: Optional[Dict[Union[int, str], str]]) -> Dict[int, str]:
        if not id2label:
            return {}
        return {int(key): value for key, value in id2label.items()}

    @staticmethod
    def _build_label2id(id2label: Dict[int, str]) -> Dict[str, int]:
        label2id: Dict[str, int] = {}
        for class_id, value in id2label.items():
            for synonym in value.split(","):
                synonym = synonym.strip()
                if synonym:
                    label2id[synonym] = int(class_id)
        return dict(sorted(label2id.items()))

    def _ensure_labels_loaded(self) -> None:
        if self._labels_loaded_from_model_index:
            return
        path = getattr(self.config, "_name_or_path", None)
        if path:
            model_index_path = Path(path).resolve() / "model_index.json"
            if model_index_path.exists():
                raw = json.loads(model_index_path.read_text(encoding="utf-8"))
                id2label = raw.get("id2label")
                if isinstance(id2label, dict):
                    self._id2label = {int(k): v for k, v in id2label.items()}
                    self.labels = self._build_label2id(self._id2label)
        self._labels_loaded_from_model_index = True

    def _normalize_class_labels(self, class_labels: Union[int, str, List[Union[int, str]]]) -> List[int]:
        if isinstance(class_labels, int):
            return [class_labels]
        if isinstance(class_labels, str):
            return self.get_label_ids(class_labels)
        if class_labels and isinstance(class_labels[0], str):
            return self.get_label_ids(class_labels)
        return list(class_labels)

    @torch.inference_mode()
    def __call__(
        self,
        class_labels: Union[int, str, List[Union[int, str]]],
        guidance_scale: float = 3.0,
        guidance_interval_min: float = 0.1,
        guidance_interval_max: float = 1.0,
        noise_scale: Optional[float] = None,
        t_eps: float = 5e-2,
        generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
        num_inference_steps: int = 1,
        height: Optional[int] = None,
        width: Optional[int] = None,
        interpolate_pos_encoding: bool = True,
        output_type: Optional[str] = "pil",
        return_dict: bool = True,
    ) -> Union[ImagePipelineOutput, Tuple]:
        r"""
        Generate images conditioned on ImageNet class ids or English labels.

        Args:
            class_labels: Class id(s), label string(s), or mixed list.
            guidance_scale: CFG scale (FD-Loss JiT-B default: 3.0).
            guidance_interval_min: Lower flow-time bound for CFG (default 0.1).
            guidance_interval_max: Upper flow-time bound for CFG (default 1.0).
            noise_scale: Initial Gaussian scale (default 1.0 at 256px).
            t_eps: Clamp for flow time in velocity denominator.
            generator: RNG for latent init.
            num_inference_steps: FD-Loss sampling steps (1 = single NFE).
            height: Output height (defaults to native `sample_size`).
            width: Output width (defaults to native `sample_size`).
            interpolate_pos_encoding: Interpolate positional embeddings for non-native sizes.
            output_type: `pil`, `np`, or `pt`.
            return_dict: Return [`ImagePipelineOutput`] if True.
        """
        # 1. Check inputs
        if num_inference_steps < 1:
            raise ValueError("num_inference_steps must be >= 1.")
        if output_type not in {"pil", "np", "pt"}:
            raise ValueError("output_type must be one of: 'pil', 'np', 'pt'.")

        # 2. Call parameters
        class_label_ids = self._normalize_class_labels(class_labels)
        batch_size = len(class_label_ids)
        generator = self._resolve_inference_generator(self._execution_device, generator)
        image_size = int(self.transformer.config.sample_size)
        patch_size = int(self.transformer.config.patch_size)
        height = int(height or image_size)
        width = int(width or image_size)
        if height % patch_size != 0 or width % patch_size != 0:
            raise ValueError(f"height/width must be divisible by patch_size={patch_size}.")
        channels = int(self.transformer.config.in_channels)
        null_class_val = int(getattr(self.transformer.config, "num_classes", 1000))
        if noise_scale is None:
            noise_scale = RECOMMENDED_NOISE_BY_SIZE.get(max(height, width), 1.0)

        # 3. Class labels
        labels = torch.tensor(class_label_ids, device=self._execution_device, dtype=torch.long).reshape(-1)
        labels = labels.clamp(0, null_class_val - 1)
        cfg_interval = self._cfg_interval(guidance_interval_min, guidance_interval_max, labels.device)

        # 4. Latents
        latents = randn_tensor(
            (batch_size, channels, height, width),
            generator=generator,
            device=self._execution_device,
            dtype=self.transformer.dtype,
        ) * noise_scale

        # 5–6. Denoising loop (FD-Loss flow t=1 → 0; generator forwarded to scheduler.step)
        self.scheduler.set_timesteps(num_inference_steps, device=self._execution_device)
        extra_step_kwargs = self.prepare_extra_step_kwargs(self.scheduler, generator=generator)
        timesteps = self.scheduler.timesteps

        for i in self.progress_bar(range(num_inference_steps)):
            t_cur = timesteps[i]
            velocity = self._predict_velocity(
                latents, t_cur, labels, guidance_scale, cfg_interval, t_eps, interpolate_pos_encoding
            )
            latents = self.scheduler.step(velocity, t_cur, latents, **extra_step_kwargs).prev_sample

        # 8. Output
        images_pt = ((latents.float().clamp(-1, 1) + 1.0) / 2.0).cpu()
        if output_type == "pt":
            images = images_pt
        elif output_type == "np":
            images = images_pt.permute(0, 2, 3, 1).numpy()
        else:
            images = self.numpy_to_pil(images_pt.permute(0, 2, 3, 1).numpy())

        self.maybe_free_model_hooks()
        if not return_dict:
            return (images,)
        return ImagePipelineOutput(images=images)