Skip to content

Pipeline API

The flashdreams.infra package defines the swappable abstractions that every integration plugs into: a config system, an encoder / diffusion-model / decoder triple, and the streaming inference pipeline that drives them.

See the Inferencing API overview for public runtime boundaries.

Config

Infra component configs are mutable InstantiateConfig dataclasses built via config.setup(). PrintableConfig provides readable configuration dumps, while derive_config deep-copies an existing config before applying nested overrides.

flashdreams.infra.config.PrintableConfig

Config base class providing a multi-line __str__ for human-readable dumps.

flashdreams.infra.config.InstantiateConfig dataclass

InstantiateConfig(_target: type)

Bases: PrintableConfig

Config carrying a _target class plus its kwargs, instantiable via setup.

setup

setup(**kwargs: Any) -> Any

Instantiate the configured object.

Source code in flashdreams/flashdreams/infra/config/base.py
33
34
35
def setup(self, **kwargs: Any) -> Any:
    """Instantiate the configured object."""
    return self._target(self, **kwargs)

flashdreams.infra.config.derive_config

derive_config(base_config: T, **changes: Any) -> T

Deep-copy a base config and apply nested keyword overrides.

Nested dict values walk into both dataclass attributes and nested dicts; leaf values overwrite directly. Raises KeyError on unknown paths.

Example:

.. code-block:: python

new_config = derive_config(
    base_config,
    tokenizer=WanVAEInterfaceConfig(checkpoint_path=...),
    dit=dict(len_t=3, checkpoint_path=...),
)
Source code in flashdreams/flashdreams/infra/config/base.py
 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
def derive_config(base_config: T, **changes: Any) -> T:
    """Deep-copy a base config and apply nested keyword overrides.

    Nested ``dict`` values walk into both dataclass attributes and nested
    dicts; leaf values overwrite directly. Raises ``KeyError`` on unknown
    paths.

    Example:

    .. code-block:: python

        new_config = derive_config(
            base_config,
            tokenizer=WanVAEInterfaceConfig(checkpoint_path=...),
            dit=dict(len_t=3, checkpoint_path=...),
        )
    """

    def _is_patchable_object(x: Any) -> bool:
        # Object is patchable if it has attribute storage (__dict__).
        return hasattr(x, "__dict__")

    def _get_field(target: Any, key: str, path: str) -> Any:
        if isinstance(target, dict):
            if key not in target:
                raise KeyError(f"Unknown key at {path}: {key}")
            return target[key]
        if hasattr(target, key):
            return getattr(target, key)
        raise KeyError(f"Unknown field at {path}: {type(target).__name__}.{key}")

    def _set_field(target: Any, key: str, value: Any, path: str) -> None:
        if isinstance(target, dict):
            if key not in target:
                raise KeyError(f"Unknown key at {path}: {key}")
            target[key] = value
            return
        if hasattr(target, key):
            setattr(target, key, value)
            return
        raise KeyError(f"Unknown field at {path}: {type(target).__name__}.{key}")

    def _recursive_patch(
        target: Any, patch: dict[str, Any], path: str = "root"
    ) -> None:
        # Apply patch recursively to dict/object target.
        for key, value in patch.items():
            current = _get_field(target, key, path)
            current_path = f"{path}.{key}"
            if isinstance(value, dict):
                # Nested patch: current can be dict or object.
                if isinstance(current, dict) or _is_patchable_object(current):
                    _recursive_patch(current, value, current_path)
                else:
                    raise TypeError(
                        f"Cannot apply nested dict patch to non-nested field at {current_path} "
                        f"(type={type(current).__name__})"
                    )
            else:
                # Leaf patch: direct assignment.
                _set_field(target, key, value, path)

    # Deep-copy base config so original config is never mutated.
    cfg = copy.deepcopy(base_config)
    _recursive_patch(cfg, changes, path=type(cfg).__name__)
    return cfg

Pipeline

The pipeline composes the model-side streaming inference stages. It autoregressively generates one chunk at a time by running an optional encoder, the diffusion model, and an optional decoder, with per-session caches for the configured streaming components. Call finalize() after generate() to advance the diffusion model's autoregressive cache.

flashdreams.infra.pipeline.StreamInferencePipelineConfig dataclass

StreamInferencePipelineConfig(*, _target: type['StreamInferencePipeline'] = (lambda: StreamInferencePipeline)(), name: str, diffusion_model: DiffusionModelConfig, decoder: DecoderConfig | None = None, encoder: EncoderConfig | None = None)

Bases: InstantiateConfig

Config for the streaming inference pipeline.

Set encoder=None when the pipeline has no per-AR-step control input (pure T2V). Set decoder=None to return the clean latent directly (training, latent-space evaluation, or pipelines that own decoding).

name instance-attribute

name: str

Stable slug for this pipeline variant; the primary key of <NAME>_CONFIGS. Runners mirror it as runner_name so flashdreams-run <slug> resolves to this pipeline.

diffusion_model instance-attribute

diffusion_model: DiffusionModelConfig

Transformer + scheduler config.

decoder class-attribute instance-attribute

decoder: DecoderConfig | None = None

Optional output :class:StreamingDecoder with a per-rollout cache, called as decoder(input, autoregressive_index, cache). Use None to return the clean latent unchanged.

encoder class-attribute instance-attribute

encoder: EncoderConfig | None = None

Optional per-AR-step input encoder. Must be a :class:StreamingEncoder; one-shot encoders go on transformer.context_encoder instead.

enable_sync_and_profile property

enable_sync_and_profile: bool

Whether to record synchronized per-stage CUDA timings.

Enabled process-wide with FLASHDREAMS_SYNC_AND_PROFILE=1

flashdreams.infra.pipeline.StreamInferencePipeline

StreamInferencePipeline(config: StreamInferencePipelineConfig)

Bases: Module, Generic[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]

End-to-end streaming inference pipeline.

Generic over the encoder, transformer, and decoder cache types. The encoder's input/output types are forwarded as Any so the transformer's predict_flow / postprocess_clean_latent overrides own the typing on the input argument they receive.

Examples:

cache = pipeline.initialize_cache(transformer_context={...})
output = pipeline.generate(0, cache, input=...)
pipeline.finalize(0, cache)
output = pipeline.generate(1, cache, input=...)
pipeline.finalize(1, cache)  # optional for the last rollout
Source code in flashdreams/flashdreams/infra/pipeline/base.py
134
135
136
137
138
139
def __init__(self, config: StreamInferencePipelineConfig) -> None:
    super().__init__()
    self.config = config
    self.encoder = config.encoder.setup() if config.encoder is not None else None
    self.decoder = config.decoder.setup() if config.decoder is not None else None
    self.diffusion_model = config.diffusion_model.setup()

initialize_cache

initialize_cache(transformer_context: dict[str, Any] | None = None, encoder_context: dict[str, Any] | None = None, decoder_context: dict[str, Any] | None = None) -> StreamInferencePipelineCache[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]

Build a fresh per-rollout cache.

Each *_context dict is forwarded as keyword arguments to the corresponding component's initialize_autoregressive_cache.

Parameters:

Name Type Description Default
transformer_context dict[str, Any] | None

Per-rollout state for the transformer (e.g. {"text_embeddings": ..., "image_embeddings": ...}).

None
encoder_context dict[str, Any] | None

Per-rollout state for the encoder. Ignored when there is no encoder.

None
decoder_context dict[str, Any] | None

Per-rollout state for the decoder. Ignored when there is no decoder.

None

Returns:

Type Description
StreamInferencePipelineCache[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]

A fresh cache to thread through generate / finalize.

Source code in flashdreams/flashdreams/infra/pipeline/base.py
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
def initialize_cache(
    self,
    transformer_context: dict[str, Any] | None = None,
    encoder_context: dict[str, Any] | None = None,
    decoder_context: dict[str, Any] | None = None,
) -> StreamInferencePipelineCache[
    StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT
]:
    """Build a fresh per-rollout cache.

    Each ``*_context`` dict is forwarded as keyword arguments to the
    corresponding component's ``initialize_autoregressive_cache``.

    Args:
        transformer_context: Per-rollout state for the transformer
            (e.g. ``{"text_embeddings": ..., "image_embeddings": ...}``).
        encoder_context: Per-rollout state for the encoder. Ignored
            when there is no encoder.
        decoder_context: Per-rollout state for the decoder. Ignored
            when there is no decoder.

    Returns:
        A fresh cache to thread through ``generate`` / ``finalize``.
    """
    transformer_context = transformer_context or {}
    encoder_context = encoder_context or {}
    decoder_context = decoder_context or {}
    return StreamInferencePipelineCache(
        encoder_cache=(
            self.encoder.initialize_autoregressive_cache(**encoder_context)
            if self.encoder is not None
            else None
        ),
        decoder_cache=(
            self.decoder.initialize_autoregressive_cache(**decoder_context)
            if self.decoder is not None
            else None
        ),
        transformer_cache=self.diffusion_model.transformer.initialize_autoregressive_cache(
            **transformer_context
        ),
    )

generate

generate(autoregressive_index: int, cache: StreamInferencePipelineCache[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT], input: Any = None) -> Tensor

Generate one chunk for this AR step.

Parameters:

Name Type Description Default
autoregressive_index int

Must be cache.autoregressive_index + 1, or 0 for the first call after initialize_cache.

required
cache StreamInferencePipelineCache[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]

Per-rollout cache from initialize_cache.

required
input Any

Raw input fed to the encoder. Required when an encoder is configured, must be None otherwise. Use NullEncoderConfig to pass an already-encoded tensor straight through.

None

Returns:

Type Description
Tensor

Decoded tensor (e.g. RGB video) when a decoder is configured;

Tensor

otherwise the unpatchified clean latent from the diffusion model.

Source code in flashdreams/flashdreams/infra/pipeline/base.py
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
@torch.no_grad()
def generate(
    self,
    autoregressive_index: int,
    cache: StreamInferencePipelineCache[
        StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT
    ],
    input: Any = None,
) -> Tensor:
    """Generate one chunk for this AR step.

    Args:
        autoregressive_index: Must be ``cache.autoregressive_index + 1``,
            or ``0`` for the first call after ``initialize_cache``.
        cache: Per-rollout cache from ``initialize_cache``.
        input: Raw input fed to the encoder. Required when an encoder
            is configured, must be ``None`` otherwise. Use
            ``NullEncoderConfig`` to pass an already-encoded tensor
            straight through.

    Returns:
        Decoded tensor (e.g. RGB video) when a decoder is configured;
        otherwise the unpatchified clean latent from the diffusion model.
    """
    return self._generate(
        autoregressive_index,
        cache,
        input=input,
    )

finalize

finalize(autoregressive_index: int, cache: StreamInferencePipelineCache[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]) -> dict[str, float] | None

Advance the diffusion AR cache for the next AR step.

Parameters:

Name Type Description Default
autoregressive_index int

Must match the index passed to the most recent generate (asserted).

required
cache StreamInferencePipelineCache[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]

Same cache used by generate. Consumes cache.final_state.

required

Returns:

Type Description
dict[str, float] | None

None unless stages were timed, either by the injected profiler

dict[str, float] | None

or by a pipeline that recorded onto cache.event_profiler.

dict[str, float] | None

Otherwise a snapshot of this AR step's per-stage timings (ms) and

dict[str, float] | None

GPU memory (GiB):

dict[str, float] | None

``{_ms, total_ms, total_ms_wo_finalize, mem_alloc_gib,

dict[str, float] | None

mem_reserved_gib, mem_peak_gib}``. The same numbers are also

dict[str, float] | None

logged via logger.info.

Source code in flashdreams/flashdreams/infra/pipeline/base.py
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
352
353
@torch.no_grad()
def finalize(
    self,
    autoregressive_index: int,
    cache: StreamInferencePipelineCache[
        StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT
    ],
) -> dict[str, float] | None:
    """Advance the diffusion AR cache for the next AR step.

    Args:
        autoregressive_index: Must match the index passed to the most
            recent ``generate`` (asserted).
        cache: Same cache used by ``generate``. Consumes
            ``cache.final_state``.

    Returns:
        ``None`` unless stages were timed, either by the injected profiler
        or by a pipeline that recorded onto ``cache.event_profiler``.
        Otherwise a snapshot of this AR step's per-stage timings (ms) and
        GPU memory (GiB):
        ``{<stage>_ms, total_ms, total_ms_wo_finalize, mem_alloc_gib,
        mem_reserved_gib, mem_peak_gib}``. The same numbers are also
        logged via ``logger.info``.
    """
    assert cache.autoregressive_index == autoregressive_index, (
        f"autoregressive_index mismatch: generate() ran with "
        f"{cache.autoregressive_index} but finalize() was called with "
        f"{autoregressive_index}."
    )
    assert cache.final_state is not None, (
        "finalize() called before generate() — no FinalState on the cache."
    )
    profiler = get_inference_profiler()
    with profiler.stage_scope(cache), profiler.range("pipeline.finalize"):
        self.diffusion_model.finalize(final_state=cache.final_state)
    if cache.event_profiler is not None:
        # A pipeline that overrides generate() times its own stages onto
        # the cache. Those are the ones to report.
        cache.event_profiler.record("finalize")
        stats_ms = cache.event_profiler.sync_and_summarize()
    else:
        with profiler.stage_scope(cache):
            stats_ms = profiler.collect_stage_ms()
    if not stats_ms:
        return None

    stats = format_result_as_ms(
        stats_ms, collect_totals=True, collect_vram_info=True
    )

    # Custom logging
    total_ms = stats["total_ms"]
    total_ms_wo_finalize = stats["total_ms_wo_finalize"]
    stages_str = " ".join(f"{stage} {ms:.3f} ms" for stage, ms in stats_ms.items())
    mem_str = ""
    if "mem_alloc_gib" in stats:
        mem_str = (
            f" | GPU mem alloc {stats['mem_alloc_gib']:.3f} GiB "
            f"reserved {stats['mem_reserved_gib']:.3f} GiB "
            f"peak {stats['mem_peak_gib']:.3f} GiB"
        )
    logger.info(
        f"AR {autoregressive_index} {stages_str} | "
        f"total(w/o finalize) {total_ms_wo_finalize:.3f} ms "
        f"total {total_ms:.3f} ms"
        f"{mem_str}"
    )
    # End of custom logging

    return stats

flashdreams.infra.pipeline.StreamInferencePipelineCache dataclass

StreamInferencePipelineCache(*, transformer_cache: TransformerCacheT, encoder_cache: StreamingEncoderCacheT | None = None, decoder_cache: StreamingDecoderCacheT | None = None, final_state: 'DiffusionModel.FinalState[TransformerCacheT] | None' = None, autoregressive_index: int | None = None, event_profiler: EventProfiler | None = None)

Bases: Generic[StreamingEncoderCacheT, TransformerCacheT, StreamingDecoderCacheT]

Per-rollout cache held by the pipeline.

transformer_cache instance-attribute

transformer_cache: TransformerCacheT

Long-lived transformer AR cache (always present).

encoder_cache class-attribute instance-attribute

encoder_cache: StreamingEncoderCacheT | None = None

Encoder AR cache; None iff the pipeline has no encoder.

decoder_cache class-attribute instance-attribute

decoder_cache: StreamingDecoderCacheT | None = None

Decoder AR cache; None iff the pipeline has no decoder.

final_state class-attribute instance-attribute

final_state: 'DiffusionModel.FinalState[TransformerCacheT] | None' = None

Diffusion-model state from the most recent generate, consumed by finalize.

autoregressive_index class-attribute instance-attribute

autoregressive_index: int | None = None

AR step index of the most recent generate.

event_profiler class-attribute instance-attribute

event_profiler: EventProfiler | None = None

Per-step profiler, populated only when profiling is on.

Diffusion model

The diffusion model wraps a transformer backbone with a denoising scheduler. Callers see only noise to clean latent conversion; the per-step flow prediction and iteration loop remain inside DiffusionModel.generate.

flashdreams.infra.diffusion.model.DiffusionModelConfig dataclass

DiffusionModelConfig(*, _target: type['DiffusionModel'] = (lambda: DiffusionModel)(), transformer: TransformerConfig, scheduler: SchedulerConfig, seed: int | None = None, context_noise: int = 0, noise_in_unpatchified_shape: bool = False)

Bases: InstantiateConfig

Config for the autoregressive diffusion model.

transformer instance-attribute

transformer: TransformerConfig

Flow-prediction network config.

scheduler instance-attribute

scheduler: SchedulerConfig

Denoising-loop config.

seed class-attribute instance-attribute

seed: int | None = None

RNG seed for initial-noise draws and scheduler sampling. None uses the global RNG.

context_noise class-attribute instance-attribute

context_noise: int = 0

Timestep used by finalize for the AR cache-update forward. 0 skips add_noise.

noise_in_unpatchified_shape class-attribute instance-attribute

noise_in_unpatchified_shape: bool = False

Debug-only: draw the initial noise in the unpatchified shape, then patchify. Slower than the default patchified path; useful when matching another implementation's RNG sequence.

flashdreams.infra.diffusion.model.DiffusionModel

DiffusionModel(config: DiffusionModelConfig)

Bases: Module, Generic[TransformerCacheT]

Autoregressive diffusion model (scheduler + transformer).

Generic over the transformer's AR cache type so user-facing typing on cache is preserved end-to-end.

Examples:

model = config.setup().to("cuda")
cache = model.transformer.initialize_autoregressive_cache(...)
clean, final_state = model.generate(autoregressive_index=0, cache=cache)
model.finalize(final_state)
Source code in flashdreams/flashdreams/infra/diffusion/model/base.py
 94
 95
 96
 97
 98
 99
100
101
def __init__(self, config: DiffusionModelConfig) -> None:
    super().__init__()
    self.config = config
    self.transformer = cast(
        Transformer[TransformerCacheT], self.config.transformer.setup()
    )
    self.scheduler = self.config.scheduler.setup()
    self._rng: torch.Generator | None = None

rng property

rng: Generator | None

Per-model generator, lazily built on the current device.

Returns None when config.seed is None. Rebuilt the first time the model's device changes after a .to(...). A device move resets the RNG stream — fine for "construct on CPU, .to(gpu) once" but mid-rollout device hops lose RNG state.

FinalState dataclass

FinalState(*, clean_latent: Tensor, autoregressive_index: int, cache: _FinalStateCacheT, input: Any = None)

Bases: Generic[_FinalStateCacheT]

State passed from generate to finalize.

clean_latent instance-attribute

clean_latent: Tensor

Patchified clean latent at the end of denoising.

autoregressive_index instance-attribute

autoregressive_index: int

AR step this state was produced at.

cache instance-attribute

cache: _FinalStateCacheT

Long-lived AR cache used during generation.

input class-attribute instance-attribute

input: Any = None

Patchified per-AR-step encoder output, or None.

generate

generate(autoregressive_index: int, cache: TransformerCacheT, input: Any = None) -> tuple[Tensor, 'DiffusionModel.FinalState[TransformerCacheT]']

Run the denoising loop for one AR step.

Parameters:

Name Type Description Default
autoregressive_index int

AR step index.

required
cache TransformerCacheT

Long-lived AR cache, mutated in place.

required
input Any

Optional per-AR-step encoder output. Patchified here and forwarded to predict_flow / postprocess_clean_latent, then stashed on the returned FinalState for finalize.

None

Returns:

Type Description
Tensor

(clean_latent, final_state). clean_latent is unpatchified;

'DiffusionModel.FinalState[TransformerCacheT]'

final_state should be passed to finalize.

Source code in flashdreams/flashdreams/infra/diffusion/model/base.py
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
def generate(
    self,
    autoregressive_index: int,
    cache: TransformerCacheT,
    input: Any = None,
) -> tuple[Tensor, "DiffusionModel.FinalState[TransformerCacheT]"]:
    """Run the denoising loop for one AR step.

    Args:
        autoregressive_index: AR step index.
        cache: Long-lived AR cache, mutated in place.
        input: Optional per-AR-step encoder output. Patchified here and
            forwarded to ``predict_flow`` / ``postprocess_clean_latent``,
            then stashed on the returned ``FinalState`` for ``finalize``.

    Returns:
        ``(clean_latent, final_state)``. ``clean_latent`` is unpatchified;
        ``final_state`` should be passed to ``finalize``.
    """
    if input is not None:
        input = self.transformer.patchify_and_maybe_split_cp(input)
    cache.start(autoregressive_index)

    if self.config.noise_in_unpatchified_shape:
        # Draw noise at the unpatchified shape so the RNG sequence matches
        # implementations that sample before patchify; ``self.latent_shape``
        # is patchified, so go through ``unpatchify_and_maybe_gather_cp``
        # first to recover the pixel-side latent shape.
        dummy_latent = torch.empty(
            self.latent_shape, device=self.device, dtype=self.dtype
        )
        dummy_latent = self.transformer.unpatchify_and_maybe_gather_cp(dummy_latent)
        initial_noise = self.transformer.initial_noise(
            latent_shape=tuple(dummy_latent.shape),
            rng=self.rng,
            cache=cache,
            input=input,
        )

    else:
        initial_noise = self.transformer.initial_noise(
            latent_shape=self.latent_shape,
            rng=self.rng,
            cache=cache,
            input=input,
        )

    def predict_flow(noisy_latent: Tensor, timestep: Tensor) -> Tensor:
        # Round-trip patchify/unpatchify around the network when the
        # scheduler is operating on unpatchified latents; the transformer
        # itself always consumes/emits patchified shapes.
        if self.config.noise_in_unpatchified_shape:
            noisy_latent = self.transformer.patchify_and_maybe_split_cp(
                noisy_latent
            )

        output = self.transformer.predict_flow(
            noisy_latent=noisy_latent,
            timestep=timestep,
            cache=cache,
            input=input,
        )

        if self.config.noise_in_unpatchified_shape:
            output = self.transformer.unpatchify_and_maybe_gather_cp(output)
        return output

    clean_latent = self.scheduler.sample(
        initial_noise=initial_noise,
        predict_flow=predict_flow,
        rng=self.rng,
    )

    if self.config.noise_in_unpatchified_shape:
        # Scheduler emitted an unpatchified latent; patchify before
        # ``postprocess_clean_latent`` and the cache-update path.
        clean_latent = self.transformer.patchify_and_maybe_split_cp(clean_latent)

    clean_latent = self.transformer.postprocess_clean_latent(
        clean_latent=clean_latent,
        cache=cache,
        input=input,
    )

    # Retain the clean state for finalization. A transformer may already
    # have refreshed specialized cache state in ``postprocess_clean_latent``.
    final_state = DiffusionModel.FinalState(
        clean_latent=clean_latent,
        autoregressive_index=autoregressive_index,
        cache=cache,
        input=input,
    )

    clean_latent = self.transformer.unpatchify_and_maybe_gather_cp(clean_latent)
    return clean_latent, final_state

finalize

finalize(final_state: 'DiffusionModel.FinalState[TransformerCacheT]') -> None

Advance the AR cache using the clean latent from generate.

Re-noises the clean latent to config.context_noise and runs the transformer's finalize_kv_cache (one forward for vanilla transformers, multiple for dual-network DiTs).

context_noise == 0 skips add_noise (sigma=0 is identity) and feeds the clean latent directly. This also dodges the requirement for schedulers to support a t=0 lookup (UniPC's inference schedule has no t=0 entry).

Source code in flashdreams/flashdreams/infra/diffusion/model/base.py
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
def finalize(
    self,
    final_state: "DiffusionModel.FinalState[TransformerCacheT]",
) -> None:
    """Advance the AR cache using the clean latent from ``generate``.

    Re-noises the clean latent to ``config.context_noise`` and runs the
    transformer's ``finalize_kv_cache`` (one forward for vanilla
    transformers, multiple for dual-network DiTs).

    ``context_noise == 0`` skips ``add_noise`` (sigma=0 is identity) and
    feeds the clean latent directly. This also dodges the requirement
    for schedulers to support a ``t=0`` lookup (UniPC's inference
    schedule has no ``t=0`` entry).
    """
    context_noise = self.config.context_noise
    timestep = torch.tensor(context_noise, device=self.device, dtype=self.dtype)
    if context_noise > 0.0:
        clean_latent = final_state.clean_latent
        # Round-trip patchify/unpatchify around ``add_noise`` so the
        # scheduler sees the same shape as in ``generate``; the cache
        # update below always consumes a patchified latent.
        if self.config.noise_in_unpatchified_shape:
            clean_latent = self.transformer.unpatchify_and_maybe_gather_cp(
                final_state.clean_latent
            )
        noisy_latent = self.scheduler.add_noise(
            clean_input=clean_latent,
            timestep=timestep,
            rng=self.rng,
        )
        if self.config.noise_in_unpatchified_shape:
            noisy_latent = self.transformer.patchify_and_maybe_split_cp(
                noisy_latent
            )
    else:
        noisy_latent = final_state.clean_latent
    self.transformer.finalize_kv_cache(
        noisy_latent=noisy_latent,
        timestep=timestep,
        cache=final_state.cache,
        input=final_state.input,
    )
    final_state.cache.finalize(final_state.autoregressive_index)

Transformer

flashdreams.infra.diffusion.transformer.TransformerConfig dataclass

TransformerConfig(*, _target: type['Transformer'] = (lambda: Transformer)())

Bases: InstantiateConfig

Category base for every flow-prediction transformer config.

flashdreams.infra.diffusion.transformer.Transformer

Transformer(config: TransformerConfig)

Bases: Module, ABC, Generic[TransformerCacheT]

Flow-prediction transformer, generic over its AR cache subclass.

Subclasses implement predict_flow and the patchify hooks. AR transformers also subclass TransformerAutoregressiveCache and override initialize_autoregressive_cache.

Example:

.. code-block:: python

class MyTransformer(Transformer[MyCache]):
    def predict_flow(self, noisy_latent, timestep, cache, input=None):
        ...

    def initialize_autoregressive_cache(self, **context) -> MyCache:
        ...
Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
78
79
80
def __init__(self, config: TransformerConfig) -> None:
    super().__init__()
    self.config = config

latent_shape abstractmethod property

latent_shape: tuple[int, ...]

Shape of the input/output latent tensor for this rank.

Includes batch dims. May depend on hierarchical context-parallel group sizes (V/T/HW), so subclasses typically derive this from self.cp_groups rather than from static config alone.

predict_flow abstractmethod

predict_flow(noisy_latent: Tensor, timestep: Tensor, cache: TransformerCacheT, input: Any = None) -> Tensor

Predict the flow at timestep.

Parameters:

Name Type Description Default
noisy_latent Tensor

Patchified noisy latent for this denoising step.

required
timestep Tensor

Scalar timestep tensor.

required
cache TransformerCacheT

Per-rollout AR cache.

required
input Any

Patchified encoder output for this AR step, or None when the pipeline has no encoder. Subclasses should narrow the type to their encoder's output type.

None

Returns:

Type Description
Tensor

Predicted flow tensor with the same shape as noisy_latent.

Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@abstractmethod
def predict_flow(
    self,
    noisy_latent: Tensor,
    timestep: Tensor,
    cache: TransformerCacheT,
    input: Any = None,
) -> Tensor:
    """Predict the flow at ``timestep``.

    Args:
        noisy_latent: Patchified noisy latent for this denoising step.
        timestep: Scalar timestep tensor.
        cache: Per-rollout AR cache.
        input: Patchified encoder output for this AR step, or ``None``
            when the pipeline has no encoder. Subclasses should narrow
            the type to their encoder's output type.

    Returns:
        Predicted flow tensor with the same shape as ``noisy_latent``.
    """

finalize_kv_cache

finalize_kv_cache(noisy_latent: Tensor, timestep: Tensor, cache: TransformerCacheT, input: Any = None) -> None

Advance the AR cache so it is ready for the next AR step.

Called by DiffusionModel.finalize after the denoising loop; the flow is discarded — only the cache side effect matters. Default runs a single predict_flow forward. Override for transformers with multiple parallel networks that must stay in lock-step.

Parameters:

Name Type Description Default
noisy_latent Tensor

Patchified latent at the AR-step's context noise (or the clean latent when context_noise == 0).

required
timestep Tensor

0-d context-noise timestep tensor.

required
cache TransformerCacheT

Per-rollout AR cache.

required
input Any

Same patchified encoder output passed to predict_flow.

None
Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def finalize_kv_cache(
    self,
    noisy_latent: Tensor,
    timestep: Tensor,
    cache: TransformerCacheT,
    input: Any = None,
) -> None:
    """Advance the AR cache so it is ready for the next AR step.

    Called by ``DiffusionModel.finalize`` after the denoising loop; the
    flow is discarded — only the cache side effect matters. Default
    runs a single ``predict_flow`` forward. Override for transformers
    with multiple parallel networks that must stay in lock-step.

    Args:
        noisy_latent: Patchified latent at the AR-step's context noise
            (or the clean latent when ``context_noise == 0``).
        timestep: 0-d context-noise timestep tensor.
        cache: Per-rollout AR cache.
        input: Same patchified encoder output passed to ``predict_flow``.
    """
    _ = self.predict_flow(noisy_latent, timestep, cache, input)

initialize_autoregressive_cache

initialize_autoregressive_cache(**context: Any) -> TransformerCacheT

Build a fresh AR cache for a new rollout.

Default returns an empty cache, correct for non-AR transformers. Subclasses with custom cache types must override this and may declare typed per-rollout context (e.g. text_embeddings).

Parameters:

Name Type Description Default
context Any

Per-rollout state forwarded as keyword arguments.

{}

Returns:

Type Description
TransformerCacheT

Fresh AR cache.

Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def initialize_autoregressive_cache(self, **context: Any) -> TransformerCacheT:
    """Build a fresh AR cache for a new rollout.

    Default returns an empty cache, correct for non-AR transformers.
    Subclasses with custom cache types must override this and may
    declare typed per-rollout context (e.g. ``text_embeddings``).

    Args:
        context: Per-rollout state forwarded as keyword arguments.

    Returns:
        Fresh AR cache.
    """
    return cast("TransformerCacheT", TransformerAutoregressiveCache())

initial_noise

initial_noise(*, latent_shape: tuple[int, ...], rng: Generator | None, cache: TransformerCacheT, input: Any = None) -> Tensor

Draw the starting latent for one denoising loop.

Default is Gaussian noise in latent_shape. Model integrations can override this to seed from encoder outputs, pin I2V frames, or attach per-step state to the cache while still using DiffusionModel.

Parameters:

Name Type Description Default
latent_shape tuple[int, ...]

Shape requested by the diffusion model.

required
rng Generator | None

Per-model generator on self.device, or None.

required
cache TransformerCacheT

Per-rollout AR cache.

required
input Any

Same patchified encoder output passed to predict_flow.

None

Returns:

Type Description
Tensor

Initial latent tensor for scheduler sampling.

Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
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
def initial_noise(
    self,
    *,
    latent_shape: tuple[int, ...],
    rng: torch.Generator | None,
    cache: TransformerCacheT,
    input: Any = None,
) -> Tensor:
    """Draw the starting latent for one denoising loop.

    Default is Gaussian noise in ``latent_shape``. Model integrations can
    override this to seed from encoder outputs, pin I2V frames, or attach
    per-step state to the cache while still using ``DiffusionModel``.

    Args:
        latent_shape: Shape requested by the diffusion model.
        rng: Per-model generator on ``self.device``, or ``None``.
        cache: Per-rollout AR cache.
        input: Same patchified encoder output passed to ``predict_flow``.

    Returns:
        Initial latent tensor for scheduler sampling.
    """
    del cache, input
    return torch.randn(
        latent_shape,
        device=self.device,
        dtype=self.dtype,
        generator=rng,
    )

postprocess_clean_latent

postprocess_clean_latent(clean_latent: Tensor, cache: TransformerCacheT, input: Any = None) -> Tensor

Optional postprocessing hook for the predicted clean latent.

Default is identity. Override to clamp or re-inject regions whose clean value is known a priori (e.g. I2V first-frame pinning). Called at the end of DiffusionModel.generate.

Parameters:

Name Type Description Default
clean_latent Tensor

Patchified x0 from the denoising loop.

required
cache TransformerCacheT

Per-rollout AR cache.

required
input Any

Same patchified encoder output passed to predict_flow.

None

Returns:

Type Description
Tensor

Postprocessed clean latent with the same shape.

Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def postprocess_clean_latent(
    self,
    clean_latent: Tensor,
    cache: TransformerCacheT,
    input: Any = None,
) -> Tensor:
    """Optional postprocessing hook for the predicted clean latent.

    Default is identity. Override to clamp or re-inject regions whose
    clean value is known a priori (e.g. I2V first-frame pinning).
    Called at the end of ``DiffusionModel.generate``.

    Args:
        clean_latent: Patchified ``x0`` from the denoising loop.
        cache: Per-rollout AR cache.
        input: Same patchified encoder output passed to ``predict_flow``.

    Returns:
        Postprocessed clean latent with the same shape.
    """
    return clean_latent

patchify_and_maybe_split_cp abstractmethod

patchify_and_maybe_split_cp(x: Any) -> Any

Patchify and (optionally) CP-split a noisy latent or encoder payload.

Tensors patchify and split. Structured payloads (e.g. an image-control struct with latent + mask) patchify each tensor field and return the same struct type. Implement as identity when neither token packing nor CP sharding applies. Output preserves the input Python type — only shapes change.

Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
213
214
215
216
217
218
219
220
221
222
@abstractmethod
def patchify_and_maybe_split_cp(self, x: Any) -> Any:
    """Patchify and (optionally) CP-split a noisy latent or encoder payload.

    Tensors patchify and split. Structured payloads (e.g. an image-control
    struct with ``latent`` + ``mask``) patchify each tensor field and
    return the same struct type. Implement as identity when neither
    token packing nor CP sharding applies. Output preserves the input
    Python type — only shapes change.
    """

unpatchify_and_maybe_gather_cp abstractmethod

unpatchify_and_maybe_gather_cp(x: Tensor) -> Tensor

Inverse of patchify_and_maybe_split_cp for the network output.

Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
224
225
226
@abstractmethod
def unpatchify_and_maybe_gather_cp(self, x: Tensor) -> Tensor:
    """Inverse of ``patchify_and_maybe_split_cp`` for the network output."""

flashdreams.infra.diffusion.transformer.TransformerAutoregressiveCache dataclass

TransformerAutoregressiveCache()

Cache that persists across an AR rollout.

Empty by default; safe to instantiate directly for non-AR transformers (default start / finalize are no-ops). Subclass and add fields plus AR bookkeeping for real per-rollout state.

Example:

.. code-block:: python

cache.start(autoregressive_index)
# one or more denoising steps...
cache.finalize(autoregressive_index)

start

start(autoregressive_index: int) -> None

Mark the start of an AR step. Default is a no-op.

Parameters:

Name Type Description Default
autoregressive_index int

Index of the AR step being started.

required
Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
37
38
39
40
41
42
def start(self, autoregressive_index: int) -> None:
    """Mark the start of an AR step. Default is a no-op.

    Args:
        autoregressive_index: Index of the AR step being started.
    """

finalize

finalize(autoregressive_index: int) -> None

Finalize bookkeeping after use at this AR step. Default is a no-op.

Parameters:

Name Type Description Default
autoregressive_index int

Index of the AR step just finalized.

required
Source code in flashdreams/flashdreams/infra/diffusion/transformer/base.py
44
45
46
47
48
49
def finalize(self, autoregressive_index: int) -> None:
    """Finalize bookkeeping after use at this AR step. Default is a no-op.

    Args:
        autoregressive_index: Index of the AR step just finalized.
    """

Schedulers

A scheduler owns the denoising loop. Its internal operations broadcast per-step scalar sigmas, so the same scheduler works for any latent layout.

flashdreams.infra.diffusion.scheduler.Scheduler

Scheduler(config: SchedulerConfig)

Bases: Module, ABC

Denoising scheduler.

Owns the entire denoising loop. Callers see only noise → clean; the loop shape (renoise / multistep / plain ODE) is private.

Concrete configs inherit SchedulerConfig and declare their own num_inference_steps / shift fields (the base holds no shared dataclass fields).

Examples:

scheduler = config.setup()
clean = scheduler.sample(initial_noise=noise, predict_flow=predictor)
noisy = scheduler.add_noise(clean_input=clean, timestep=t)
Source code in flashdreams/flashdreams/infra/diffusion/scheduler/base.py
50
51
52
def __init__(self, config: SchedulerConfig) -> None:
    super().__init__()
    self.config = config

sample abstractmethod

sample(initial_noise: Tensor, predict_flow: FlowPredictor, rng: Generator | None = None) -> Tensor

Run the full denoising loop and return the clean latent.

Schedulers are shape-agnostic: every internal op broadcasts against per-step scalar sigmas. In practice initial_noise is a video latent [B, C, T, H, W], conventionally treated as a sample at sigma=1.

Parameters:

Name Type Description Default
initial_noise Tensor

Gaussian noise on the caller's device/dtype.

required
predict_flow FlowPredictor

Per-step closure invoked num_inference_steps times.

required
rng Generator | None

Generator on the same device. Used by self-forcing renoise loops; pure ODE solvers ignore it.

None

Returns:

Type Description
Tensor

Clean latent with the same shape, device, and dtype as

Tensor

initial_noise.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/base.py
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
@abstractmethod
def sample(
    self,
    initial_noise: Tensor,
    predict_flow: FlowPredictor,
    rng: torch.Generator | None = None,
) -> Tensor:
    """Run the full denoising loop and return the clean latent.

    Schedulers are shape-agnostic: every internal op broadcasts against
    per-step scalar sigmas. In practice ``initial_noise`` is a video
    latent ``[B, C, T, H, W]``, conventionally treated as a sample at
    ``sigma=1``.

    Args:
        initial_noise: Gaussian noise on the caller's device/dtype.
        predict_flow: Per-step closure invoked
            ``num_inference_steps`` times.
        rng: Generator on the same device. Used by self-forcing renoise
            loops; pure ODE solvers ignore it.

    Returns:
        Clean latent with the same shape, device, and dtype as
        ``initial_noise``.
    """

add_noise abstractmethod

add_noise(clean_input: Tensor, timestep: Tensor, rng: Generator | None = None) -> Tensor

Apply the forward corruption x_t = (1 - sigma(t)) * x_0 + sigma(t) * eps.

Timestep value semantics are scheduler-specific: all schedulers snap to the nearest entry of their inference schedule.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/base.py
80
81
82
83
84
85
86
87
88
89
90
91
@abstractmethod
def add_noise(
    self,
    clean_input: Tensor,
    timestep: Tensor,
    rng: torch.Generator | None = None,
) -> Tensor:
    """Apply the forward corruption ``x_t = (1 - sigma(t)) * x_0 + sigma(t) * eps``.

    Timestep value semantics are scheduler-specific: all schedulers snap
    to the nearest entry of their inference schedule.
    """

flashdreams.infra.diffusion.scheduler.SchedulerConfig dataclass

SchedulerConfig(*, _target: type['Scheduler'] = (lambda: Scheduler)())

Bases: InstantiateConfig

Category base for every denoising-scheduler config.

flashdreams.infra.diffusion.scheduler.FlowPredictor

Bases: Protocol

Closure (noisy_latent, timestep) -> predicted_flow.

Built by DiffusionModel.generate by binding the per-AR-step cache / input to the transformer's predict_flow. A scheduler invokes it once per denoising iteration. The scheduler decides the timestep dtype (UniPC uses int64, flow-match uses float).

__call__

__call__(noisy_latent: Tensor, timestep: Tensor) -> Tensor

Predict the flow at timestep.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/base.py
28
29
30
def __call__(self, noisy_latent: Tensor, timestep: Tensor) -> Tensor:
    """Predict the flow at ``timestep``."""
    ...

flashdreams.infra.diffusion.scheduler.FlowMatchSchedulerConfig dataclass

FlowMatchSchedulerConfig(*, _target: type['FlowMatchScheduler'] = (lambda: FlowMatchScheduler)(), num_inference_steps: int = 4, shift: float = 8.0, denoising_timesteps: list[int] = (lambda: [1000, 750, 500, 250])(), warp_denoising_step: bool = True, num_train_timesteps: int = 1000, sigma_max: float = 1.0, sigma_min: float = 0.0, extra_one_step: bool = True, timestep_dtype: dtype = float32, reference_sampling: bool = False, enable_tqdm: bool = False)

Bases: SchedulerConfig

Config for the flow-matching scheduler.

num_inference_steps class-attribute instance-attribute

num_inference_steps: int = 4

Number of denoising steps; must equal len(denoising_timesteps).

shift class-attribute instance-attribute

shift: float = 8.0

Schedule warp factor.

denoising_timesteps class-attribute instance-attribute

denoising_timesteps: list[int] = field(default_factory=lambda: [1000, 750, 500, 250])

Per-step diffusion timesteps in [0, num_train_timesteps].

warp_denoising_step class-attribute instance-attribute

warp_denoising_step: bool = True

Map denoising_timesteps through the warped sigma schedule.

num_train_timesteps class-attribute instance-attribute

num_train_timesteps: int = 1000

Length of the training sigma table.

sigma_max class-attribute instance-attribute

sigma_max: float = 1.0

Top of the linspace before warping; 1.0 matches DiffSynth, upstream Wan / Lingbot ships 0.999.

sigma_min class-attribute instance-attribute

sigma_min: float = 0.0

Bottom of the linspace before warping. Reserved for upstream parity; only 0.0 is exercised.

extra_one_step class-attribute instance-attribute

extra_one_step: bool = True

If True, build the schedule from linspace(sigma_max, sigma_min, N+1)[:-1] (matches DiffSynth / upstream Wan); False uses N points and is kept for non-Wan recipes.

timestep_dtype class-attribute instance-attribute

timestep_dtype: dtype = torch.float32

Dtype of denoising_step_list.

Set to an integer dtype (e.g. torch.int64) when the network's time embedding is sensitive to the fractional part of the warped timestep — upstream Wan stores scheduler.timesteps as int64 and lets the embedding upcast to float64 internally.

reference_sampling class-attribute instance-attribute

reference_sampling: bool = False

Keep timesteps exact and recurrent samples in the network flow dtype.

LingBot's reference noise bank is FP32 while its network is BF16. This mode preserves integer timestep values and rounds each x0 before re-noising.

enable_tqdm class-attribute instance-attribute

enable_tqdm: bool = False

Whether to enable tqdm progress bar.

flashdreams.infra.diffusion.scheduler.FlowMatchScheduler

FlowMatchScheduler(config: FlowMatchSchedulerConfig)

Bases: Scheduler

Flow-matching scheduler with self-forcing renoise (DiffSynth-style).

Each iteration converts the predicted flow to an x0 estimate, then re-noises at the same sigma to feed the next iteration. The final x0 is returned::

x_t = initial_noise
for t in denoising_step_list:
    v = predict_flow(x_t, t)
    x0 = x_t - sigma(t) * v
    x_t = (1 - sigma(t)) * x0 + sigma(t) * eps
return x0

Example:

.. code-block:: python

scheduler = FlowMatchSchedulerConfig(
    num_inference_steps=4,
    shift=8.0,
    denoising_timesteps=[1000, 750, 500, 250],
).setup().to("cuda")
clean = scheduler.sample(initial_noise=noise, predict_flow=fn)

Schedule buffers are pinned to fp32 even after module.to(bf16); integer timesteps like 1000 would otherwise round to 1024.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/fm.py
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
def __init__(self, config: FlowMatchSchedulerConfig) -> None:
    super().__init__(config)
    self.config: FlowMatchSchedulerConfig = config

    N = config.num_train_timesteps
    assert config.num_inference_steps == len(config.denoising_timesteps), (
        f"num_inference_steps ({config.num_inference_steps}) must equal "
        f"len(denoising_timesteps) ({len(config.denoising_timesteps)})"
    )
    # Full warped schedule: identical to DiffSynth's
    #   sigmas = linspace(1, 0, N + 1)[:-1]
    #   sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
    # (matches reference exactly in fp32).
    if config.extra_one_step:
        sigmas = torch.linspace(
            config.sigma_max, config.sigma_min, N + 1, dtype=torch.float32
        )[:-1]
    else:
        sigmas = torch.linspace(
            config.sigma_max, config.sigma_min, N, dtype=torch.float32
        )
    full_sigmas = _warp(sigmas, config.shift)
    full_timesteps = full_sigmas * N

    # Pre-resolve per-step (sigma, timestep) so sample() does no
    # per-step argmin. Replicates the legacy resolution exactly:
    #   - warp_denoising_step=True: denoising_step_list[i] is read
    #     from a (full_timesteps ++ [0.0]) buffer at index
    #     N - denoising_timesteps[i]. The legacy then argmin's that
    #     value against the 1000-entry full_timesteps to get sigma.
    #     For idx in [0, N), the answer is full_sigmas[idx]; the
    #     idx==N corner (only hit when denoising_timesteps[i]==0)
    #     argmin's 0.0 to the smallest entry, full_sigmas[N-1].
    #   - warp_denoising_step=False: denoising_step_list[i] is the
    #     raw int; legacy argmin's it against full_timesteps and
    #     returns the snapped sigma.
    idxs = [N - t for t in config.denoising_timesteps]
    if config.warp_denoising_step:
        step_list = [full_timesteps[idx].item() if idx < N else 0.0 for idx in idxs]
        sigma_list = [full_sigmas[idx if idx < N else N - 1].item() for idx in idxs]
    else:
        step_list = [float(t) for t in config.denoising_timesteps]
        snapped_idx = [
            int(torch.argmin((full_timesteps - t).abs()).item()) for t in step_list
        ]
        sigma_list = [full_sigmas[i].item() for i in snapped_idx]

    # Buffers move with .to(device) but are pinned to fp32 by the
    # ``_apply`` override below -- a stray ``model.to(bf16)`` would
    # otherwise round integer timesteps (1000 -> 1024) and quantize
    # the sigma table.
    self.register_buffer(
        "denoising_step_list",
        torch.tensor(step_list, dtype=config.timestep_dtype),
        persistent=False,
    )
    self.register_buffer(
        "denoising_sigmas",
        torch.tensor(sigma_list, dtype=torch.float32),
        persistent=False,
    )
    # Full table only used by add_noise (rare path, called from
    # finalize when context_noise > 0).
    self.register_buffer("_full_sigmas", full_sigmas, persistent=False)
    self.register_buffer("_full_timesteps", full_timesteps, persistent=False)

sample

sample(initial_noise: Tensor, predict_flow: FlowPredictor, rng: Generator | None = None) -> Tensor

Run the self-forcing flow-match denoising loop.

Iteration 0 trusts initial_noise as the sigma=1 sample; later iterations re-noise the previous x0 estimate to the new sigma before the network forward. By default the result is cast back to initial_noise.dtype after every step; reference sampling casts each x0 to the network flow dtype.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/fm.py
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
def sample(
    self,
    initial_noise: Tensor,
    predict_flow: FlowPredictor,
    rng: torch.Generator | None = None,
) -> Tensor:
    """Run the self-forcing flow-match denoising loop.

    Iteration 0 trusts ``initial_noise`` as the ``sigma=1`` sample;
    later iterations re-noise the previous ``x0`` estimate to the new
    sigma before the network forward. By default the result is cast back
    to ``initial_noise.dtype`` after every step; reference sampling casts
    each ``x0`` to the network flow dtype.
    """
    input_dtype = initial_noise.dtype
    reference_sampling = self.config.reference_sampling
    sigmas = self.denoising_sigmas
    timesteps = self.denoising_step_list

    noisy = initial_noise
    clean: Tensor | None = None
    for i in tqdm(
        range(timesteps.shape[0]),
        disable=not self.config.enable_tqdm,
        desc="FlowMatchScheduler",
    ):
        sigma = sigmas[i]
        # Schedule buffers are pinned to fp32 (to preserve integer
        # timestep values under a stray `module.to(bf16)`), but the
        # By default the network expects timesteps in the input dtype.
        # Reference sampling preserves configured integer values rather
        # than rounding them through bf16.
        timestep = (
            timesteps[i]
            if reference_sampling
            else timesteps[i].to(dtype=input_dtype)
        )
        if i > 0:
            assert clean is not None
            if reference_sampling:
                noise = torch.empty_like(clean).normal_(generator=rng)
                state_dtype = clean.dtype
            else:
                noise = torch.empty_like(noisy).normal_(generator=rng)
                state_dtype = input_dtype
            noisy = ((1.0 - sigma) * clean + sigma * noise).to(state_dtype)
        flow = predict_flow(noisy, timestep)
        if reference_sampling:
            clean = (noisy - sigma * flow).to(flow.dtype)
        else:
            clean = noisy - sigma * flow
    assert clean is not None, "denoising_step_list is empty"
    return clean if reference_sampling else clean.to(input_dtype)

add_noise

add_noise(clean_input: Tensor, timestep: Tensor, rng: Generator | None = None) -> Tensor

Apply the forward corruption at an arbitrary timestep.

Snaps timestep to the nearest entry of the warped training table and uses it as sigma in the standard lerp.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/fm.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def add_noise(
    self,
    clean_input: Tensor,
    timestep: Tensor,
    rng: torch.Generator | None = None,
) -> Tensor:
    """Apply the forward corruption at an arbitrary timestep.

    Snaps ``timestep`` to the nearest entry of the warped training table
    and uses it as sigma in the standard lerp.
    """
    assert timestep.shape == (), f"expected scalar timestep, got {timestep.shape}"
    full_t = self._full_timesteps
    idx = torch.argmin((full_t - timestep.to(full_t.dtype)).abs()).reshape(1)
    sigma = self._full_sigmas.index_select(0, idx).reshape(())
    noise = torch.empty_like(clean_input).normal_(generator=rng)
    return ((1.0 - sigma) * clean_input + sigma * noise).to(clean_input.dtype)

flashdreams.infra.diffusion.scheduler.FlowMatchEulerDiscreteSchedulerConfig dataclass

FlowMatchEulerDiscreteSchedulerConfig(*, _target: type['FlowMatchEulerDiscreteScheduler'] = (lambda: FlowMatchEulerDiscreteScheduler)(), num_inference_steps: int = 4, shift: float = 5.0, num_train_timesteps: int = 1000, fixed_timesteps: tuple[float, ...] | None = None, enable_tqdm: bool = False)

Bases: SchedulerConfig

Config for the flow-matching Euler-discrete scheduler.

Defaults match diffusers' :class:FlowMatchEulerDiscreteScheduler with num_train_timesteps=1000 and the standard linspace + shift warp. Set :attr:fixed_timesteps to a length num_inference_steps + 1 tuple (terminal value typically 0.0) to pin an externally-derived distilled schedule.

num_inference_steps class-attribute instance-attribute

num_inference_steps: int = 4

Number of Euler steps. Matches the distilled WAN-5B 4-step path out of the box; bump to 30-50 for non-distilled checkpoints.

shift class-attribute instance-attribute

shift: float = 5.0

Schedule warp factor applied to the linspaced sigma grid. Ignored when :attr:fixed_timesteps is set.

num_train_timesteps class-attribute instance-attribute

num_train_timesteps: int = 1000

Length of the training sigma table. Used to convert between integer timesteps and [0, 1] sigmas (sigma = timestep / N).

fixed_timesteps class-attribute instance-attribute

fixed_timesteps: tuple[float, ...] | None = None

Override the derived schedule with a precomputed list of timesteps. When set, must have length num_inference_steps + 1; the trailing entry is typically 0.0 so the last Euler step lands on the clean latent. None (default) derives the schedule from :attr:num_inference_steps + :attr:shift via the standard flow-matching linspace + warp.

The HY-WorldPlay distilled WAN-5B path pins this to (1000.0, 960.0, 888.8889, 727.2728, 0.0).

enable_tqdm class-attribute instance-attribute

enable_tqdm: bool = False

Whether to enable the tqdm progress bar inside :meth:sample.

flashdreams.infra.diffusion.scheduler.FlowMatchUniPCSchedulerConfig dataclass

FlowMatchUniPCSchedulerConfig(*, _target: type['FlowMatchUniPCScheduler'] = (lambda: FlowMatchUniPCScheduler)(), num_inference_steps: int = 50, shift: float = 5.0, num_train_timesteps: int = 1000, solver_order: int = 2, use_kerras_sigma: bool = False, enable_tqdm: bool = False)

Bases: SchedulerConfig

Config for the flow-matching UniPC scheduler.

Defaults match the official Wan 2.1 inference integration (UniPC, BH2, order 2, shift 5.0). Override shift per checkpoint as recommended upstream (e.g. 3.0 for Wan 2.1 14B I2V 480P).

num_inference_steps class-attribute instance-attribute

num_inference_steps: int = 50

Number of UniPC denoising steps.

shift class-attribute instance-attribute

shift: float = 5.0

Schedule warp factor.

num_train_timesteps class-attribute instance-attribute

num_train_timesteps: int = 1000

Length of the training sigma table.

solver_order class-attribute instance-attribute

solver_order: int = 2

UniPC solver order; only 2 is supported.

use_kerras_sigma class-attribute instance-attribute

use_kerras_sigma: bool = False

Whether to use the exact sigma used in edm sampler.

enable_tqdm class-attribute instance-attribute

enable_tqdm: bool = False

Whether to enable tqdm progress bar.

flashdreams.infra.diffusion.scheduler.FlowMatchUniPCScheduler

FlowMatchUniPCScheduler(config: FlowMatchUniPCSchedulerConfig)

Bases: Scheduler

Order-2 UniPC predictor-corrector for flow-matching.

Specialized + pre-baked variant of the upstream Wan 2.1 UniPC solver. Schedule buffers (sigmas + per-step coefficients) stay fp32 regardless of module.to(dtype).

Example:

.. code-block:: python

scheduler = FlowMatchUniPCSchedulerConfig(
    num_inference_steps=50,
    shift=5.0,
).setup().to("cuda")
clean = scheduler.sample(initial_noise=noise, predict_flow=fn)
Source code in flashdreams/flashdreams/infra/diffusion/scheduler/fm_unipc.py
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
def __init__(self, config: FlowMatchUniPCSchedulerConfig) -> None:
    super().__init__(config)
    self.config: FlowMatchUniPCSchedulerConfig = config
    assert config.solver_order == 2, (
        f"Only solver_order=2 is supported (got {config.solver_order}); "
        "the slim UniPC bakes coefficients for the order-1/order-2 "
        "warmup+steady-state pattern. Use the legacy scheduler if you "
        "need other orders."
    )
    N = config.num_inference_steps
    N_train = config.num_train_timesteps

    if self.config.use_kerras_sigma:
        # force to use the exact sigma used in edm sampler
        sigma_max = 200
        sigma_min = 0.01
        rho = 7
        sigmas = np.arange(N + 1) / N
        min_inv_rho = sigma_min ** (1 / rho)
        max_inv_rho = sigma_max ** (1 / rho)
        sigmas = (max_inv_rho + sigmas * (min_inv_rho - max_inv_rho)) ** rho
        sigmas_inf = sigmas / (1 + sigmas)
    else:
        # Schedule build: identical to upstream
        # ``set_timesteps(num_inference_steps, shift=config.shift)``:
        #
        #   sigmas = linspace(sigma_max, sigma_min, N + 1)[:-1]
        #   sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
        #   sigmas = concat(sigmas, [0.0])
        #
        # where (sigma_min, sigma_max) come from the
        # ``alphas = linspace(1, 1/num_train_timesteps, num_train_timesteps)[::-1]``
        # / ``sigmas = 1 - alphas`` table built at construction.
        # Training-time sigma table: ``alphas = linspace(1, 1/N, N)[::-1]``
        # then ``sigmas = 1 - alphas``. Reference builds this with
        # ``shift=1.0`` (no warp); only the inference schedule below
        # gets warped with ``config.shift``.
        train_alphas = np.linspace(1.0, 1.0 / N_train, N_train)[::-1].copy()
        train_sigmas = 1.0 - train_alphas
        sigma_min, sigma_max = float(train_sigmas[-1]), float(train_sigmas[0])
        sigmas_inf = np.linspace(sigma_max, sigma_min, N + 1)[:-1]
        sigmas_inf = (
            config.shift * sigmas_inf / (1.0 + (config.shift - 1.0) * sigmas_inf)
        )

    # Match reference: timesteps come from the fp64 ``sigmas * N``
    # cast directly to int64 (truncation), not via fp32. The fp32
    # round-trip can shift entries by 1 LSB and break the
    # exact-match index lookup in ``add_noise``.
    timesteps_fp64 = sigmas_inf * N_train
    sigmas_full = np.concatenate([sigmas_inf, [0.0]]).astype(np.float32)

    coefs = _build_per_step_coefs(sigmas_full)

    # Buffers move with .to(device) but the float ones are pinned
    # to fp32 by the ``_apply`` override below -- a stray
    # ``model.to(bf16)`` would otherwise quantize the snap table
    # and the per-step coefficients. ``timesteps`` stays int64
    # (PyTorch already skips int casts in ``module.to(dtype)``).
    self.register_buffer(
        "timesteps",
        torch.from_numpy(timesteps_fp64).to(torch.int64),
        persistent=False,
    )
    # ``sigmas[i]`` is the noise level at step i (used for convert
    # = sample - sigma * model_output).
    self.register_buffer(
        "sigmas",
        torch.from_numpy(sigmas_full[:-1].copy()),
        persistent=False,
    )
    self.register_buffer(
        "_sigmas_full",
        torch.from_numpy(sigmas_full),
        persistent=False,
    )
    for k, v in coefs.items():
        self.register_buffer(k, torch.from_numpy(v), persistent=False)
    self._FP32_BUFFERS = ("sigmas", "_sigmas_full", *coefs.keys())

sample

sample(initial_noise: Tensor, predict_flow: FlowPredictor, rng: Generator | None = None) -> Tensor

Run the order-2 UniPC predictor-corrector denoising loop.

Each iteration: network → flow → x0 → corrector (skipped at step 0) → predictor. All per-step coefficients are pre-baked at construction; the loop is pure tensor ops. Internal arithmetic is fp32; the result is cast back to initial_noise.dtype. rng is unused (deterministic ODE) but accepted for interface conformance.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/fm_unipc.py
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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def sample(
    self,
    initial_noise: Tensor,
    predict_flow: FlowPredictor,
    rng: torch.Generator | None = None,
) -> Tensor:
    """Run the order-2 UniPC predictor-corrector denoising loop.

    Each iteration: network → flow → ``x0`` → corrector (skipped at
    step 0) → predictor. All per-step coefficients are pre-baked at
    construction; the loop is pure tensor ops. Internal arithmetic is
    fp32; the result is cast back to ``initial_noise.dtype``. ``rng``
    is unused (deterministic ODE) but accepted for interface conformance.
    """
    input_dtype = initial_noise.dtype
    N = self.timesteps.shape[0]

    sample = initial_noise
    m_prev: Tensor | None = None
    m_prev_prev: Tensor | None = None
    last_sample: Tensor | None = None

    for i in tqdm(
        range(N),
        disable=not self.config.enable_tqdm,
        desc="FlowMatchUniPCScheduler",
    ):
        # Schedule buffers are pinned to fp32 (to preserve integer
        # timestep values under a stray `module.to(bf16)`), but the
        # network expects timesteps in the input dtype so that
        # downstream modulation / Linear layers stay consistent.
        timestep = self.timesteps[i].to(dtype=input_dtype)

        # Network forward (heavy compute -- everything else here is
        # ~free relative to this).
        flow = predict_flow(sample, timestep)

        # Convert to x0 estimate (predict_x0 + flow_prediction):
        #   m_curr = sample - sigma_i * flow
        # Promote to fp32 to match upstream's
        # ``model_output = model_output.to(dtype=torch.float32)``
        # before convert_model_output.
        m_curr = sample.to(torch.float32) - self.sigmas[i] * flow.to(torch.float32)

        # Corrector (skip on first step).
        #
        # last_sample is the sample saved before the previous
        # predictor (matches upstream ``self.last_sample``).
        # m_prev is model_outputs[-1] (previous step's m_curr),
        # m_prev_prev is model_outputs[-2] (2 steps back).
        # b_corr_dprev[1] = 0 (order-1 corrector) so the
        # m_prev_prev term vanishes at i=1; we alias it to m_prev
        # there to skip the zero-tensor allocation.
        if i >= 1:
            assert last_sample is not None and m_prev is not None
            m_pp = m_prev_prev if m_prev_prev is not None else m_prev
            corrected = (
                self.a_corr[i] * last_sample.to(torch.float32)
                + self.b_corr_m0[i] * m_prev
                + self.b_corr_dprev[i] * (m_pp - m_prev)
                + self.b_corr_dt[i] * (m_curr - m_prev)
            )
            sample = corrected.to(input_dtype)

        # Save sample BEFORE the predictor so the next iteration's
        # corrector can use it as "last_sample".
        last_sample = sample

        # Predictor advances sample to next sigma:
        #   x_{i+1} = a_pred[i] * x_i + b_pred_m0[i] * m0
        #            + b_pred_dprev[i] * (m_prev - m0)
        # b_pred_dprev[0] = 0 (order-1 warmup) and
        # b_pred_dprev[-1] = 0 (order-1 final) so the same line
        # serves both order branches; alias m_prev to m_curr at
        # the warmup step to skip a zero-tensor allocation.
        m_p = m_prev if m_prev is not None else m_curr
        predicted = (
            self.a_pred[i] * sample.to(torch.float32)
            + self.b_pred_m0[i] * m_curr
            + self.b_pred_dprev[i] * (m_p - m_curr)
        )
        sample = predicted.to(input_dtype)

        # Roll the model-output history.
        m_prev_prev = m_prev
        m_prev = m_curr

    return sample

add_noise

add_noise(clean_input: Tensor, timestep: Tensor, rng: Generator | None = None) -> Tensor

Apply the forward corruption at an arbitrary timestep.

Snaps timestep to the nearest entry of the inference schedule on-device (no Python sync) and uses the matching sigma in the lerp.

Source code in flashdreams/flashdreams/infra/diffusion/scheduler/fm_unipc.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def add_noise(
    self,
    clean_input: Tensor,
    timestep: Tensor,
    rng: torch.Generator | None = None,
) -> Tensor:
    """Apply the forward corruption at an arbitrary timestep.

    Snaps ``timestep`` to the nearest entry of the inference schedule
    on-device (no Python sync) and uses the matching sigma in the lerp.
    """
    assert timestep.shape == (), f"expected scalar timestep, got {timestep.shape}"
    ts = self.timesteps
    idx = torch.argmin((ts - timestep.to(ts.dtype)).abs()).reshape(1)
    sigma = self._sigmas_full.index_select(0, idx).reshape(())
    noise = torch.empty_like(clean_input).normal_(generator=rng)
    return ((1.0 - sigma) * clean_input + sigma * noise).to(clean_input.dtype)

Encoder

Encoders turn raw conditioning into latent tensors. Encoder is stateless and one-shot, while StreamingEncoder carries per-step state. The video variant adds spatial and temporal size contracts.

flashdreams.infra.encoder.EncoderConfig dataclass

EncoderConfig(*, _target: type[Encoder] = (lambda: Encoder)())

Bases: InstantiateConfig

Category base for every encoder config (stateless or streaming).

flashdreams.infra.encoder.Encoder

Encoder(config: EncoderConfig)

Bases: ABC, Module

Stateless encoder.

forward is not pinned by the base. Encoders used as a context_encoder (one-shot, called once inside :meth:Transformer.initialize_autoregressive_cache) must match the slim call shape forward(self, input).

For per-AR-step encoders that need a per-rollout cache, inherit from :class:StreamingEncoder instead.

Source code in flashdreams/flashdreams/infra/encoder/base.py
46
47
48
def __init__(self, config: "EncoderConfig") -> None:
    super().__init__()
    self.config = config

flashdreams.infra.encoder.StreamingEncoder

StreamingEncoder(config: EncoderConfig)

Bases: ABC, Module, Generic[StreamingEncoderCacheT]

Streaming encoder, generic over the per-rollout cache type.

forward is not pinned by the base. Streaming encoders called by :class:StreamInferencePipeline must match its call shape: forward(self, input, autoregressive_index=0, cache=None).

Source code in flashdreams/flashdreams/infra/encoder/base.py
82
83
84
def __init__(self, config: "EncoderConfig") -> None:
    super().__init__()
    self.config = config

initialize_autoregressive_cache abstractmethod

initialize_autoregressive_cache(**context: Any) -> StreamingEncoderCacheT

Build a fresh per-rollout cache.

Override to return the encoder's concrete cache type.

Source code in flashdreams/flashdreams/infra/encoder/base.py
86
87
88
89
90
91
@abstractmethod
def initialize_autoregressive_cache(self, **context: Any) -> StreamingEncoderCacheT:
    """Build a fresh per-rollout cache.

    Override to return the encoder's concrete cache type.
    """

flashdreams.infra.encoder.StreamingVideoEncoder

StreamingVideoEncoder(config: EncoderConfig)

Bases: StreamingEncoder[StreamingEncoderCacheT]

Streaming pixel-video encoder.

Pins down the contracts that every streaming pixel→latent video encoder satisfies in addition to :class:StreamingEncoder:

  • Spatial and temporal compression ratios between the pixel and latent grids (constants of the architecture).
  • AR-step-aware temporal size mappers, so a pipeline can size its inputs and outputs without knowing the encoder's concrete temporal cache topology (causal first-frame padding, sliding windows, etc.).

Spatial scaling is trivially side // spatial_compression_ratio in either direction; the AR-step-asymmetric piece is the temporal size, which gets its own mapper. Typically AR 0 takes 1 + (T_lat - 1) * r pixel frames to produce T_lat latent frames because of causal first-frame padding, while AR ≥ 1 takes T_lat * r pixel frames.

Source code in flashdreams/flashdreams/infra/encoder/base.py
82
83
84
def __init__(self, config: "EncoderConfig") -> None:
    super().__init__()
    self.config = config

spatial_compression_ratio abstractmethod property

spatial_compression_ratio: int

Pixel side ÷ latent side. Constant across AR steps.

temporal_compression_ratio abstractmethod property

temporal_compression_ratio: int

Pixel frames ÷ latent frames in steady state (AR ≥ 1).

AR 0 typically takes one extra (un-grouped) pixel frame to produce its first latent frame because of causal first-frame padding; that asymmetry lives inside :meth:get_output_temporal_size / :meth:get_input_temporal_size.

get_output_temporal_size abstractmethod

get_output_temporal_size(autoregressive_index: int, input_temporal_size: int) -> int

Latent frame count produced from input_temporal_size pixel frames.

Parameters:

Name Type Description Default
autoregressive_index int

AR step index (0-based).

required
input_temporal_size int

Number of pixel frames fed at this step.

required

Returns:

Type Description
int

Number of latent frames emitted at this step.

Source code in flashdreams/flashdreams/infra/encoder/base.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@abstractmethod
def get_output_temporal_size(
    self,
    autoregressive_index: int,
    input_temporal_size: int,
) -> int:
    """Latent frame count produced from ``input_temporal_size`` pixel frames.

    Args:
        autoregressive_index: AR step index (0-based).
        input_temporal_size: Number of pixel frames fed at this step.

    Returns:
        Number of latent frames emitted at this step.
    """

get_input_temporal_size abstractmethod

get_input_temporal_size(autoregressive_index: int, output_temporal_size: int) -> int

Pixel frame count needed to produce output_temporal_size latents.

Inverse of :meth:get_output_temporal_size. Implementations should assert output_temporal_size is achievable at this AR step (i.e. the corresponding pixel count comes out as a positive integer).

Parameters:

Name Type Description Default
autoregressive_index int

AR step index (0-based).

required
output_temporal_size int

Desired number of latent frames.

required

Returns:

Type Description
int

Number of pixel frames needed at this step.

Source code in flashdreams/flashdreams/infra/encoder/base.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@abstractmethod
def get_input_temporal_size(
    self,
    autoregressive_index: int,
    output_temporal_size: int,
) -> int:
    """Pixel frame count needed to produce ``output_temporal_size`` latents.

    Inverse of :meth:`get_output_temporal_size`. Implementations
    should assert ``output_temporal_size`` is achievable at this AR
    step (i.e. the corresponding pixel count comes out as a
    positive integer).

    Args:
        autoregressive_index: AR step index (0-based).
        output_temporal_size: Desired number of latent frames.

    Returns:
        Number of pixel frames needed at this step.
    """

flashdreams.infra.encoder.StreamingEncoderCache dataclass

StreamingEncoderCache()

Per-rollout cache for :class:StreamingEncoder.

Empty by default; subclass to add fields (e.g. last-frame latent, cross-step accumulators).

flashdreams.infra.encoder.NullEncoderConfig dataclass

NullEncoderConfig(*, _target: type[NullEncoder] = (lambda: NullEncoder)())

Bases: EncoderConfig

Config for the identity encoder.

flashdreams.infra.encoder.NullEncoder

NullEncoder(config: EncoderConfig)

Bases: Encoder

Identity encoder: returns its input unchanged.

Wire as the transformer's context_encoder slot to pass already- encoded tensors straight to the diffusion model.

Example:

.. code-block:: python

config = TransformerConfig(
    context_encoder=NullEncoderConfig(),
    ...,
)
Source code in flashdreams/flashdreams/infra/encoder/base.py
46
47
48
def __init__(self, config: "EncoderConfig") -> None:
    super().__init__()
    self.config = config

forward

forward(input: Any) -> Any

Return input unchanged.

Source code in flashdreams/flashdreams/infra/encoder/base.py
193
194
195
def forward(self, input: Any) -> Any:
    """Return ``input`` unchanged."""
    return input

Decoder

Decoders turn emitted latents into frames. StreamingDecoder carries per-rollout state; the video variant adds spatial and temporal size contracts.

flashdreams.infra.decoder.DecoderConfig dataclass

DecoderConfig(*, _target: type[StreamingDecoder] = (lambda: StreamingDecoder)())

Bases: InstantiateConfig

Category base for every decoder config.

flashdreams.infra.decoder.StreamingDecoder

StreamingDecoder(config: DecoderConfig)

Bases: ABC, Module, Generic[StreamingDecoderCacheT]

Streaming decoder, generic over the per-rollout cache type.

forward is not pinned by the base. Streaming decoders called by :class:StreamInferencePipeline must match its call shape: forward(self, input, autoregressive_index=0, cache=None).

Source code in flashdreams/flashdreams/infra/decoder/base.py
55
56
57
def __init__(self, config: "DecoderConfig") -> None:
    super().__init__()
    self.config = config

initialize_autoregressive_cache abstractmethod

initialize_autoregressive_cache(**context: Any) -> StreamingDecoderCacheT

Build a fresh per-rollout cache.

Override to return the decoder's concrete cache type.

Source code in flashdreams/flashdreams/infra/decoder/base.py
59
60
61
62
63
64
@abstractmethod
def initialize_autoregressive_cache(self, **context: Any) -> StreamingDecoderCacheT:
    """Build a fresh per-rollout cache.

    Override to return the decoder's concrete cache type.
    """

flashdreams.infra.decoder.StreamingVideoDecoder

StreamingVideoDecoder(config: DecoderConfig)

Bases: StreamingDecoder[StreamingDecoderCacheT]

Streaming pixel-video decoder.

Pins down the contracts that every streaming latent→pixel video decoder satisfies in addition to :class:StreamingDecoder:

  • Spatial and temporal compression ratios between the latent and pixel grids (constants of the architecture).
  • AR-step-aware temporal size mappers, so a pipeline can size its inputs and outputs without knowing the decoder's concrete temporal cache topology (causal first-frame padding, sliding windows, etc.).

Spatial scaling is trivially side * spatial_compression_ratio in either direction; the AR-step-asymmetric piece is the temporal size, which gets its own mapper. Typically AR 0 produces fewer pixel frames per latent frame than AR ≥ 1 because of causal first-frame padding.

Source code in flashdreams/flashdreams/infra/decoder/base.py
55
56
57
def __init__(self, config: "DecoderConfig") -> None:
    super().__init__()
    self.config = config

spatial_compression_ratio abstractmethod property

spatial_compression_ratio: int

Pixel side ÷ latent side. Constant across AR steps.

temporal_compression_ratio abstractmethod property

temporal_compression_ratio: int

Pixel frames ÷ latent frames in steady state (AR ≥ 1).

AR 0 typically yields fewer pixel frames per latent frame because of causal first-frame padding; that asymmetry lives inside :meth:get_output_temporal_size / :meth:get_input_temporal_size.

get_output_temporal_size abstractmethod

get_output_temporal_size(autoregressive_index: int, input_temporal_size: int) -> int

Pixel frame count produced by input_temporal_size latent frames.

Parameters:

Name Type Description Default
autoregressive_index int

AR step index (0-based).

required
input_temporal_size int

Number of latent frames fed at this step.

required

Returns:

Type Description
int

Number of pixel frames emitted at this step.

Source code in flashdreams/flashdreams/infra/decoder/base.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@abstractmethod
def get_output_temporal_size(
    self,
    autoregressive_index: int,
    input_temporal_size: int,
) -> int:
    """Pixel frame count produced by ``input_temporal_size`` latent frames.

    Args:
        autoregressive_index: AR step index (0-based).
        input_temporal_size: Number of latent frames fed at this step.

    Returns:
        Number of pixel frames emitted at this step.
    """

get_input_temporal_size abstractmethod

get_input_temporal_size(autoregressive_index: int, output_temporal_size: int) -> int

Latent frame count needed to produce output_temporal_size pixels.

Inverse of :meth:get_output_temporal_size. Implementations should assert output_temporal_size is achievable at this AR step (i.e. divisible by the right ratio after subtracting any causal padding).

Parameters:

Name Type Description Default
autoregressive_index int

AR step index (0-based).

required
output_temporal_size int

Desired number of pixel frames.

required

Returns:

Type Description
int

Number of latent frames needed at this step.

Source code in flashdreams/flashdreams/infra/decoder/base.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@abstractmethod
def get_input_temporal_size(
    self,
    autoregressive_index: int,
    output_temporal_size: int,
) -> int:
    """Latent frame count needed to produce ``output_temporal_size`` pixels.

    Inverse of :meth:`get_output_temporal_size`. Implementations
    should assert ``output_temporal_size`` is achievable at this AR
    step (i.e. divisible by the right ratio after subtracting any
    causal padding).

    Args:
        autoregressive_index: AR step index (0-based).
        output_temporal_size: Desired number of pixel frames.

    Returns:
        Number of latent frames needed at this step.
    """

flashdreams.infra.decoder.StreamingDecoderCache dataclass

StreamingDecoderCache()

Per-rollout cache for :class:StreamingDecoder.

Empty by default; subclass to add fields (e.g. temporal feature buffers carried across AR steps).