inferencekit-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""LTX-2.x through Diffusers behind the clip server: text-to-video and image-to-video.
|
|
2
|
+
|
|
3
|
+
python -m inferencekit.engines.clip.server --pipeline diffusers_ltx --model Lightricks/LTX-2.5 \\
|
|
4
|
+
--task t2v --dtype bf16 [--offload model] [--attention native] \\
|
|
5
|
+
[--pipeline-arg reference=true --pipeline-arg default_negative_prompt=true]
|
|
6
|
+
|
|
7
|
+
What was checked, and how (Diffusers 0.41.0.dev0 from the main branch, read on 15 Oct 2026; run on a GPU host on
|
|
8
|
+
LTX-2 (15 Oct) and LTX-2.5 (16 Oct)):
|
|
9
|
+
* Classes: ``diffusers.LTX2Pipeline`` (t2v) and ``diffusers.LTX2ImageToVideoPipeline`` (i2v), both in
|
|
10
|
+
diffusers/pipelines/ltx2. Both take prompt, negative_prompt, height, width, num_frames, frame_rate,
|
|
11
|
+
num_inference_steps, guidance_scale, generator, output_type, return_dict, callback_on_step_end; i2v
|
|
12
|
+
also takes image. With return_dict=False the call returns (video, audio); output_type="np" gives
|
|
13
|
+
float frames in [0, 1], shape [batch, frames, height, width, 3]; audio is a waveform
|
|
14
|
+
[batch, channels, samples] at ``pipe.vocoder.config.output_sampling_rate``.
|
|
15
|
+
* Sizes: height and width must be multiples of 32 (check_inputs); frames are 8k+1 (the VAE's temporal
|
|
16
|
+
ratio); the latent video is [1, transformer.config.in_channels, (frames-1)//8+1, height//32, width//32].
|
|
17
|
+
* Noise: randn_tensor draws on the generator's device, so a CPU generator gives the same numbers on any
|
|
18
|
+
GPU. With ``generator=torch.Generator("cpu").manual_seed(noise_seed)`` the pipeline's first draw is
|
|
19
|
+
the video noise and the second the audio noise, both float32: the recipe in noise.py. The pipeline
|
|
20
|
+
draws the noise itself rather than taking ``latents`` because in i2v a ``latents`` argument
|
|
21
|
+
replaces the image-conditioned latents (the image would be lost), and audio ``latents`` would be
|
|
22
|
+
renormalised. Hooks on prepare_latents / prepare_audio_latents compare what the pipeline drew with
|
|
23
|
+
the server's CPU tensors on every clip: ``effective.noise_check`` is true only when they are equal.
|
|
24
|
+
* Muxing: ``diffusers.utils.encode_video`` (PyAV, libx264 yuv420p, AAC audio). It writes an integer
|
|
25
|
+
frame rate (``rate=int(fps)``), so a fractional fps is refused instead of being silently rounded.
|
|
26
|
+
|
|
27
|
+
BF16 reference mode (``--pipeline-arg reference=true``): every component in BF16, PyTorch
|
|
28
|
+
scaled-dot-product attention (backend ``native``, set on the transformer and the text connectors, so a
|
|
29
|
+
``DIFFUSERS_ATTN_BACKEND`` in the environment cannot change it), the default pipeline class, no
|
|
30
|
+
``call.*`` overrides, no VAE tiling (tiles are blended, which changes the frames) and no prompt
|
|
31
|
+
enhancement. It is the pipeline as shipped, the side that quality clip compares the others against.
|
|
32
|
+
Offloading moves weights but changes no numbers, so it is allowed in reference mode. Reference mode also
|
|
33
|
+
needs an explicit negative prompt (``default_negative_prompt=true`` or ``negative_prompt=TEXT``; an empty
|
|
34
|
+
TEXT for none): without one Diffusers encodes an empty one, and nothing would say which the clips used.
|
|
35
|
+
|
|
36
|
+
Other options (``--pipeline-arg``): ``pipeline_class=NAME`` (another diffusers class with the same call
|
|
37
|
+
arguments), ``negative_prompt=TEXT`` or ``default_negative_prompt=true`` (Diffusers' LTX-2 DEFAULT_NEGATIVE_PROMPT,
|
|
38
|
+
the one the model cards use), ``revision=REV``, ``vae_tiling=true``, ``call.NAME=VALUE`` (an extra __call__
|
|
39
|
+
argument, e.g. ``call.decode_timestep=0.05``; never one the request sets), ``transformer_subfolder=NAME`` (load
|
|
40
|
+
the transformer from another folder of the checkpoint) and ``scheduler=JSON`` (scheduler config overrides).
|
|
41
|
+
|
|
42
|
+
LTX-2.5-Diffusers (from its model card, then run on its weights on 16 Oct 2026): ``transformer/`` is the DISTILLED
|
|
43
|
+
DiT and model_index.json loads it by default, with a scheduler set up for distilled sigma schedules. The full
|
|
44
|
+
BF16 model is ``transformer_subfolder=transformer_full`` with ``scheduler={"use_dynamic_shifting": true,
|
|
45
|
+
"shift_terminal": 0.1}``. Reference mode refuses a local folder that has transformer_full/ unless
|
|
46
|
+
transformer_subfolder is given, so a reference never runs the distilled DiT by accident: a hub id is looked up
|
|
47
|
+
in the Hugging Face cache (offline, at ``revision``), and reference mode refuses a hub id it cannot find there.
|
|
48
|
+
In reference mode a transformer_subfolder needs the scheduler overrides that go with it (use_dynamic_shifting
|
|
49
|
+
and shift_terminal: the checkpoint's own scheduler is set up for the distilled sigmas). These options are
|
|
50
|
+
allowed in reference mode (they pick the checkpoint and its schedule; they are recorded in describe(), and
|
|
51
|
+
a re-run of the manifest can compare that with the reference settings it expects).
|
|
52
|
+
|
|
53
|
+
Loading (first run on real LTX-2.5 weights, RTX PRO 6000, 16 Oct 2026): a hub id with ``revision`` set to a full
|
|
54
|
+
40-character commit sha (or with HF_HUB_OFFLINE set) is loaded from its Hugging Face cache snapshot folder when it
|
|
55
|
+
is there, because Diffusers' sharded-checkpoint loader asks the Hub for the file list even offline and fails
|
|
56
|
+
(OfflineModeIsEnabled). A branch or tag name online is left to from_pretrained: the cache's ref for it may point
|
|
57
|
+
at an older snapshot. describe() records the revision asked for and the snapshot's commit (``snapshot_commit``), so
|
|
58
|
+
a reference manifest's expect_config can pin the commit that was loaded. LTX-2.5-Diffusers' model_index.json also lists optional components a clip never uses:
|
|
59
|
+
the Gemma-4 prompt enhancer (about 10 GB) with its processor, and the duration head (only used when num_frames
|
|
60
|
+
is left out). They are passed as None (``skipped_components`` in describe()): loaded, they would only take GPU
|
|
61
|
+
memory, and on a host with only the folders a clip loads, loading them fails.
|
|
62
|
+
|
|
63
|
+
Per-step dumps (stepdump.py) and replay: the scheduler class's ``step`` is wrapped for the length of each pipeline call
|
|
64
|
+
(the pipeline steps the video latents with ``pipe.scheduler`` and the audio latents with a deep copy of it, so an
|
|
65
|
+
instance attribute would be copied into the audio scheduler still bound to the video one; the class-level wrapper
|
|
66
|
+
tells them apart by identity). Every clip records, per step and stream, the latent the step started from, the
|
|
67
|
+
guided velocity the scheduler took and the latent it returned (unpacked to the noise layout) and the scheduler's
|
|
68
|
+
sigmas. ``replay`` is the checker's side (quality clip-steps): the reference pipeline runs a request with an engine's
|
|
69
|
+
dump forced in: prepare_latents returns the dump's first latents, and every scheduler step returns the dump's next
|
|
70
|
+
latent, so each step's transformer passes see the engine's x_t; it reports the reference's own guided output at
|
|
71
|
+
every step, the reference scheduler's update of the engine's output (the chain check), and the frames its own
|
|
72
|
+
decode path makes from the dump's final latent.
|
|
73
|
+
|
|
74
|
+
Stages (self-reported, see server.py): text_encode (encode_prompt + connectors), prepare_latents (in i2v
|
|
75
|
+
this includes encoding the image), denoise (+ denoise_steps), vae_decode, postprocess (to frames),
|
|
76
|
+
audio_decode (audio VAE + vocoder), mux.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
from __future__ import annotations
|
|
80
|
+
|
|
81
|
+
import dataclasses
|
|
82
|
+
import hashlib
|
|
83
|
+
import io
|
|
84
|
+
import re
|
|
85
|
+
from contextlib import contextmanager
|
|
86
|
+
|
|
87
|
+
from inferencekit.engines.clip import stepdump
|
|
88
|
+
from inferencekit.engines.clip.server import (BadRequest, ClipJob, Generated, PipelineOptions, StageClock,
|
|
89
|
+
hook_method, timed)
|
|
90
|
+
|
|
91
|
+
DTYPES = {"bf16": "bfloat16", "fp16": "float16", "fp32": "float32"}
|
|
92
|
+
CLASSES = {"t2v": "LTX2Pipeline", "i2v": "LTX2ImageToVideoPipeline"}
|
|
93
|
+
# __call__ arguments the request sets: a call.* override may not touch them.
|
|
94
|
+
REQUEST_ARGS = {"prompt", "negative_prompt", "image", "height", "width", "num_frames", "frame_rate",
|
|
95
|
+
"num_inference_steps", "guidance_scale", "generator", "latents", "audio_latents", "output_type",
|
|
96
|
+
"return_dict", "callback_on_step_end", "callback_on_step_end_tensor_inputs"}
|
|
97
|
+
OPTIONS = {"reference", "pipeline_class", "negative_prompt", "default_negative_prompt", "revision", "vae_tiling",
|
|
98
|
+
"transformer_subfolder", "scheduler"}
|
|
99
|
+
# Optional pipeline components a clip never uses (prompt enhancement is off; num_frames is always given), passed as
|
|
100
|
+
# None so from_pretrained does not load them (see the module docstring).
|
|
101
|
+
UNUSED_COMPONENTS = ("prompt_enhancer", "processor", "duration_head")
|
|
102
|
+
COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") # a Hub revision that names one commit (a branch or tag can move)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _dtype_name(dtype) -> str:
|
|
106
|
+
s = str(dtype).replace("torch.", "")
|
|
107
|
+
return {v: k for k, v in DTYPES.items()}.get(s, s)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_options(opts: PipelineOptions) -> bool:
|
|
111
|
+
"""ValueError for options this factory does not take or reference mode forbids; True in reference mode."""
|
|
112
|
+
unknown = sorted(k for k in opts.extra if k not in OPTIONS and not k.startswith("call."))
|
|
113
|
+
if unknown:
|
|
114
|
+
raise ValueError(f"unknown --pipeline-arg {unknown}; known: {sorted(OPTIONS)} and call.NAME")
|
|
115
|
+
if opts.dtype not in DTYPES:
|
|
116
|
+
raise ValueError(f"--dtype must be one of {sorted(DTYPES)}")
|
|
117
|
+
if opts.extra.get("default_negative_prompt") and opts.extra.get("negative_prompt") is not None:
|
|
118
|
+
raise ValueError("give negative_prompt or default_negative_prompt=true, not both")
|
|
119
|
+
sched = opts.extra.get("scheduler")
|
|
120
|
+
if sched is not None and not isinstance(sched, dict):
|
|
121
|
+
raise ValueError("scheduler must be a JSON object of scheduler config overrides, e.g. "
|
|
122
|
+
'{"use_dynamic_shifting": true, "shift_terminal": 0.1}')
|
|
123
|
+
reference = bool(opts.extra.get("reference"))
|
|
124
|
+
if not reference:
|
|
125
|
+
return False
|
|
126
|
+
bad = [k for k in opts.extra if k.startswith("call.") or k in ("vae_tiling", "pipeline_class")]
|
|
127
|
+
if opts.dtype != "bf16" or opts.attention not in (None, "native") or bad:
|
|
128
|
+
raise ValueError("reference mode is BF16 with native attention, the default pipeline class and "
|
|
129
|
+
"no call.* overrides or VAE tiling")
|
|
130
|
+
if opts.extra.get("negative_prompt") is None and not opts.extra.get("default_negative_prompt"):
|
|
131
|
+
raise ValueError("reference mode needs an explicit negative prompt: --pipeline-arg default_negative_prompt=true "
|
|
132
|
+
"(Diffusers' LTX-2 DEFAULT_NEGATIVE_PROMPT) or --pipeline-arg negative_prompt=TEXT (empty "
|
|
133
|
+
"for none)")
|
|
134
|
+
sub = opts.extra.get("transformer_subfolder")
|
|
135
|
+
if sub:
|
|
136
|
+
missing = [k for k in ("use_dynamic_shifting", "shift_terminal") if k not in (sched or {})]
|
|
137
|
+
if missing:
|
|
138
|
+
raise ValueError(f"reference mode with transformer_subfolder={sub} needs the scheduler overrides that go "
|
|
139
|
+
f"with it (missing {', '.join(missing)}): the checkpoint's own scheduler is set up for "
|
|
140
|
+
"its default (distilled) transformer, e.g. --pipeline-arg 'scheduler="
|
|
141
|
+
"{\"use_dynamic_shifting\": true, \"shift_terminal\": 0.1}'")
|
|
142
|
+
return reference
|
|
143
|
+
full = _has_full_transformer(opts.model, opts.extra.get("revision"))
|
|
144
|
+
if full is None:
|
|
145
|
+
raise ValueError(f"reference mode cannot tell whether {opts.model} ships a transformer_full/ folder (its default "
|
|
146
|
+
"transformer/ would then be a distilled one): it is not a local folder and not in the Hugging "
|
|
147
|
+
"Face cache at this revision. Download it first, or give the folder")
|
|
148
|
+
if full:
|
|
149
|
+
raise ValueError(f"{opts.model} has a transformer_full/ folder: its default transformer/ is the distilled "
|
|
150
|
+
"one (LTX-2.5-Diffusers), not the BF16 full model a reference needs. Pass "
|
|
151
|
+
"--pipeline-arg transformer_subfolder=transformer_full and the scheduler the full model "
|
|
152
|
+
"needs, e.g. --pipeline-arg 'scheduler={\"use_dynamic_shifting\": true, "
|
|
153
|
+
"\"shift_terminal\": 0.1}'")
|
|
154
|
+
return reference
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _offline() -> bool:
|
|
158
|
+
import os
|
|
159
|
+
|
|
160
|
+
return os.environ.get("HF_HUB_OFFLINE", "").strip().lower() in ("1", "true", "yes", "on")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _cached_snapshot(model: str, revision: str | None = None) -> str | None:
|
|
164
|
+
"""The folder to load ``model`` from without calling the Hub: ``model`` itself when it is a local folder; for a hub
|
|
165
|
+
id whose ``revision`` is a full commit sha (or any hub id with HF_HUB_OFFLINE set), its Hugging Face cache snapshot
|
|
166
|
+
folder when it is there and holds a model_index.json. None otherwise (from_pretrained then resolves the hub id
|
|
167
|
+
itself, online: a branch or tag name such as ``main`` may have moved since the cache last saw it)."""
|
|
168
|
+
import os
|
|
169
|
+
|
|
170
|
+
if os.path.isdir(str(model)):
|
|
171
|
+
return str(model)
|
|
172
|
+
if not (revision and COMMIT_SHA.match(str(revision))) and not _offline():
|
|
173
|
+
return None # unpinned, or a branch or tag name, online: the cache's ref may point at an older snapshot
|
|
174
|
+
try:
|
|
175
|
+
from huggingface_hub import snapshot_download
|
|
176
|
+
|
|
177
|
+
folder = snapshot_download(str(model), revision=revision, local_files_only=True)
|
|
178
|
+
except Exception: # noqa: BLE001 - not installed, not a valid id, or not in the cache
|
|
179
|
+
return None
|
|
180
|
+
if not os.path.isfile(os.path.join(str(folder), "model_index.json")):
|
|
181
|
+
return None # not a Diffusers pipeline snapshot (e.g. only some files of it were fetched)
|
|
182
|
+
return str(folder)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _snapshot_commit(folder: str | None) -> str | None:
|
|
186
|
+
"""The commit a Hugging Face cache snapshot folder holds (its name, .../snapshots/<sha>), else None."""
|
|
187
|
+
import os
|
|
188
|
+
|
|
189
|
+
if not folder:
|
|
190
|
+
return None
|
|
191
|
+
name = os.path.basename(os.path.normpath(str(folder)))
|
|
192
|
+
parent = os.path.basename(os.path.dirname(os.path.normpath(str(folder))))
|
|
193
|
+
return name if parent == "snapshots" and COMMIT_SHA.match(name) else None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _has_full_transformer(model: str, revision: str | None = None) -> bool | None:
|
|
197
|
+
"""Whether a Diffusers checkpoint ships a second, full transformer beside its default one (LTX-2.5-Diffusers:
|
|
198
|
+
transformer/ is the distilled DiT, transformer_full/ the full one). A local folder is looked at directly; a hub
|
|
199
|
+
id in its Hugging Face cache snapshot at ``revision`` (offline: local_files_only). None when neither is there.
|
|
200
|
+
A snapshot cached without transformer_full/ reads as False: a re-run that checks the expected settings catches that."""
|
|
201
|
+
import os
|
|
202
|
+
|
|
203
|
+
if os.path.isdir(str(model)):
|
|
204
|
+
return os.path.isdir(os.path.join(str(model), "transformer_full"))
|
|
205
|
+
try:
|
|
206
|
+
from huggingface_hub import snapshot_download
|
|
207
|
+
|
|
208
|
+
folder = snapshot_download(str(model), revision=str(revision) if revision else None, local_files_only=True)
|
|
209
|
+
except Exception: # noqa: BLE001 - not installed, not a valid id, or not in the cache
|
|
210
|
+
return None
|
|
211
|
+
return os.path.isdir(os.path.join(str(folder), "transformer_full"))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def configure(pipe, opts: PipelineOptions, reference: bool) -> PipelineOptions:
|
|
215
|
+
"""Place a loaded pipeline (device, offload), set its attention backend and VAE tiling.
|
|
216
|
+
|
|
217
|
+
Returns ``opts`` with the attention backend that was set (None: Diffusers' default, which
|
|
218
|
+
``DIFFUSERS_ATTN_BACKEND`` can change). Reference mode sets ``native`` explicitly.
|
|
219
|
+
"""
|
|
220
|
+
if opts.offload == "model":
|
|
221
|
+
pipe.enable_model_cpu_offload(device=opts.device)
|
|
222
|
+
elif opts.offload == "sequential":
|
|
223
|
+
pipe.enable_sequential_cpu_offload(device=opts.device)
|
|
224
|
+
else:
|
|
225
|
+
pipe.to(opts.device)
|
|
226
|
+
attention = opts.attention or ("native" if reference else None)
|
|
227
|
+
if attention:
|
|
228
|
+
pipe.transformer.set_attention_backend(attention)
|
|
229
|
+
connectors = getattr(pipe, "connectors", None) # the text connectors attend too (LTX2Attention)
|
|
230
|
+
if hasattr(connectors, "set_attention_backend"):
|
|
231
|
+
connectors.set_attention_backend(attention)
|
|
232
|
+
if opts.extra.get("vae_tiling"):
|
|
233
|
+
pipe.vae.enable_tiling()
|
|
234
|
+
return dataclasses.replace(opts, attention=attention)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def make_pipeline(opts: PipelineOptions) -> LTXDiffusers:
|
|
238
|
+
reference = check_options(opts) # before any import or load: a refused option costs nothing
|
|
239
|
+
import diffusers
|
|
240
|
+
import torch
|
|
241
|
+
|
|
242
|
+
cls_name = opts.extra.get("pipeline_class") or CLASSES[opts.task]
|
|
243
|
+
cls = getattr(diffusers, cls_name)
|
|
244
|
+
revision = str(opts.extra["revision"]) if opts.extra.get("revision") else None
|
|
245
|
+
source = _cached_snapshot(opts.model, revision) # a local folder: no Hub call while loading
|
|
246
|
+
kw = {"torch_dtype": getattr(torch, DTYPES[opts.dtype])}
|
|
247
|
+
if revision and source is None: # a hub id not in the cache: from_pretrained fetches that revision
|
|
248
|
+
kw["revision"] = revision
|
|
249
|
+
load_from = source or opts.model
|
|
250
|
+
sub = opts.extra.get("transformer_subfolder")
|
|
251
|
+
if sub: # e.g. LTX-2.5-Diffusers' transformer_full/ (its model_index.json names the distilled transformer/)
|
|
252
|
+
tkw = {k: v for k, v in kw.items()}
|
|
253
|
+
kw["transformer"] = diffusers.LTX2VideoTransformer3DModel.from_pretrained(load_from, subfolder=str(sub),
|
|
254
|
+
**tkw)
|
|
255
|
+
skipped = [c for c in UNUSED_COMPONENTS if c in (getattr(cls, "_optional_components", None) or ())]
|
|
256
|
+
pipe = cls.from_pretrained(load_from, **kw, **{c: None for c in skipped})
|
|
257
|
+
if opts.extra.get("scheduler"):
|
|
258
|
+
pipe.scheduler = type(pipe.scheduler).from_config(pipe.scheduler.config, **opts.extra["scheduler"])
|
|
259
|
+
if opts.extra.get("default_negative_prompt"):
|
|
260
|
+
from diffusers.pipelines.ltx2.utils import DEFAULT_NEGATIVE_PROMPT
|
|
261
|
+
|
|
262
|
+
opts = dataclasses.replace(opts, extra={**opts.extra, "negative_prompt": DEFAULT_NEGATIVE_PROMPT})
|
|
263
|
+
opts = configure(pipe, opts, reference)
|
|
264
|
+
facts = {"pipeline": f"diffusers:{cls_name}", "diffusers_version": diffusers.__version__,
|
|
265
|
+
"torch_version": torch.__version__, "reference": reference,
|
|
266
|
+
"transformer_subfolder": sub or "transformer", "skipped_components": skipped,
|
|
267
|
+
"loaded_from": "hf_cache_snapshot" if source and source != opts.model else
|
|
268
|
+
("folder" if source else "hub"),
|
|
269
|
+
"revision": revision, "snapshot_commit": _snapshot_commit(source) if source != opts.model else None,
|
|
270
|
+
"scheduler": {"class": type(pipe.scheduler).__name__,
|
|
271
|
+
**{k: pipe.scheduler.config.get(k) for k in ("use_dynamic_shifting", "shift_terminal",
|
|
272
|
+
"shift") if k in pipe.scheduler.config}}}
|
|
273
|
+
if str(opts.device).startswith("cuda") and torch.cuda.is_available():
|
|
274
|
+
facts["gpu"] = torch.cuda.get_device_name(torch.device(opts.device))
|
|
275
|
+
return LTXDiffusers(pipe, opts, torch, facts)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _numel(shape) -> int:
|
|
279
|
+
n = 1
|
|
280
|
+
for d in shape:
|
|
281
|
+
n *= int(d)
|
|
282
|
+
return n
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _torch_dtype(t) -> str:
|
|
286
|
+
return str(t.dtype).replace("torch.", "")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _prev(res):
|
|
290
|
+
"""The new latent in a scheduler step's result (a tuple with return_dict=False, else an output object)."""
|
|
291
|
+
return res[0] if isinstance(res, tuple) else res.prev_sample
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _with_prev(res, latent):
|
|
295
|
+
if isinstance(res, tuple):
|
|
296
|
+
return (latent, *res[1:])
|
|
297
|
+
res.prev_sample = latent
|
|
298
|
+
return res
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _active_attention() -> str | None:
|
|
302
|
+
try: # private in Diffusers: the backend used when a model sets none
|
|
303
|
+
from diffusers.models.attention_dispatch import _AttentionBackendRegistry
|
|
304
|
+
|
|
305
|
+
backend = _AttentionBackendRegistry.get_active_backend()[0]
|
|
306
|
+
return getattr(backend, "value", str(backend))
|
|
307
|
+
except Exception: # noqa: BLE001
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class LTXDiffusers:
|
|
312
|
+
"""The clip server's pipeline object for a loaded Diffusers LTX-2.x pipeline (``pipe``)."""
|
|
313
|
+
|
|
314
|
+
def __init__(self, pipe, opts: PipelineOptions, torch_mod, facts: dict | None = None):
|
|
315
|
+
self.pipe, self.opts, self.torch = pipe, opts, torch_mod
|
|
316
|
+
self.facts = dict(facts or {})
|
|
317
|
+
self.negative_prompt = opts.extra.get("negative_prompt")
|
|
318
|
+
self.call_kwargs = {k[5:]: v for k, v in opts.extra.items() if k.startswith("call.")}
|
|
319
|
+
clash = sorted(set(self.call_kwargs) & REQUEST_ARGS)
|
|
320
|
+
if clash:
|
|
321
|
+
raise ValueError(f"call.* may not set what the request sets: {clash}")
|
|
322
|
+
self.clock: StageClock | None = None
|
|
323
|
+
self._noise: dict = {}
|
|
324
|
+
self._noise_seen: dict = {}
|
|
325
|
+
self._install_hooks()
|
|
326
|
+
|
|
327
|
+
# Hooks are installed once; they time on self.clock while a clip runs and call straight through otherwise.
|
|
328
|
+
# Modules get torch forward hooks, not a replaced ``forward``: with --offload model, Diffusers re-applies
|
|
329
|
+
# the offload hooks after every call (maybe_free_model_hooks), and removing them restores ``forward``.
|
|
330
|
+
def _install_hooks(self) -> None:
|
|
331
|
+
p, cur = self.pipe, lambda: self.clock
|
|
332
|
+
hook_method(p, "encode_prompt", "text_encode", cur)
|
|
333
|
+
if getattr(p, "connectors", None) is not None:
|
|
334
|
+
self._hook_module(p.connectors, "text_encode")
|
|
335
|
+
for attr, check in (("prepare_latents", self._check_video), ("prepare_audio_latents", self._check_audio)):
|
|
336
|
+
if hasattr(p, attr):
|
|
337
|
+
setattr(p, attr, self._capturing(timed(getattr(p, attr), "prepare_latents", cur), check))
|
|
338
|
+
|
|
339
|
+
def loop_start(module, args): # returns None: a pre-hook's return value would replace the inputs
|
|
340
|
+
if self.clock is not None:
|
|
341
|
+
self.clock.denoise_start()
|
|
342
|
+
|
|
343
|
+
p.transformer.register_forward_pre_hook(loop_start)
|
|
344
|
+
hook_method(p.vae, "decode", "vae_decode", cur)
|
|
345
|
+
hook_method(p.video_processor, "postprocess_video", "postprocess", cur)
|
|
346
|
+
if getattr(p, "audio_vae", None) is not None:
|
|
347
|
+
hook_method(p.audio_vae, "decode", "audio_decode", cur)
|
|
348
|
+
if getattr(p, "vocoder", None) is not None:
|
|
349
|
+
self._hook_module(p.vocoder, "audio_decode")
|
|
350
|
+
|
|
351
|
+
def _hook_module(self, module, stage: str) -> None:
|
|
352
|
+
def pre(mod, args):
|
|
353
|
+
if self.clock is not None:
|
|
354
|
+
self.clock.begin()
|
|
355
|
+
mod._ik_clock = self.clock # close the stage on the clock it was opened on
|
|
356
|
+
|
|
357
|
+
def post(mod, args, output):
|
|
358
|
+
clock = mod.__dict__.pop("_ik_clock", None)
|
|
359
|
+
if clock is not None:
|
|
360
|
+
clock.end(stage)
|
|
361
|
+
|
|
362
|
+
module.register_forward_pre_hook(pre)
|
|
363
|
+
module.register_forward_hook(post)
|
|
364
|
+
|
|
365
|
+
def _capturing(self, fn, check):
|
|
366
|
+
def wrapper(*args, **kwargs):
|
|
367
|
+
out = fn(*args, **kwargs)
|
|
368
|
+
if self.clock is not None:
|
|
369
|
+
check(out)
|
|
370
|
+
return out
|
|
371
|
+
|
|
372
|
+
return wrapper
|
|
373
|
+
|
|
374
|
+
def _check_video(self, out) -> None:
|
|
375
|
+
latents, mask = out if isinstance(out, tuple) else (out, None)
|
|
376
|
+
ref = self._noise.get("video")
|
|
377
|
+
if ref is None:
|
|
378
|
+
return
|
|
379
|
+
p = self.pipe
|
|
380
|
+
ref = p._pack_latents(ref, p.transformer_spatial_patch_size, p.transformer_temporal_patch_size)
|
|
381
|
+
ref = ref.to(latents.device, latents.dtype)
|
|
382
|
+
if ref.shape != latents.shape:
|
|
383
|
+
self._noise_seen["video"] = False
|
|
384
|
+
return
|
|
385
|
+
if mask is not None: # i2v: the image's tokens (mask 1) are not noise
|
|
386
|
+
free = mask[0] == 0
|
|
387
|
+
latents, ref = latents[:, free], ref[:, free]
|
|
388
|
+
self._noise_seen["video"] = bool(self.torch.equal(latents, ref))
|
|
389
|
+
|
|
390
|
+
def _check_audio(self, out) -> None:
|
|
391
|
+
ref = self._noise.get("audio")
|
|
392
|
+
if ref is None:
|
|
393
|
+
return
|
|
394
|
+
ref = self.pipe._pack_audio_latents(ref).to(out.device, out.dtype)
|
|
395
|
+
self._noise_seen["audio"] = ref.shape == out.shape and bool(self.torch.equal(out, ref))
|
|
396
|
+
|
|
397
|
+
# ------------------------------------------------------------------ the ClipPipeline interface
|
|
398
|
+
|
|
399
|
+
def describe(self) -> dict:
|
|
400
|
+
dtype = _dtype_name(getattr(self.pipe.transformer, "dtype", self.opts.dtype))
|
|
401
|
+
attention = self.opts.attention or _active_attention() or "default"
|
|
402
|
+
neg = self.negative_prompt
|
|
403
|
+
return {**self.facts, "model": self.opts.model, "task": self.opts.task, "dtype": dtype,
|
|
404
|
+
"attention": attention, "device": self.opts.device, "offload": self.opts.offload,
|
|
405
|
+
"negative_prompt_sha256": hashlib.sha256(str(neg).encode()).hexdigest() if neg is not None else None,
|
|
406
|
+
"call_kwargs": self.call_kwargs}
|
|
407
|
+
|
|
408
|
+
def noise_shapes(self, job: ClipJob) -> dict:
|
|
409
|
+
p = self.pipe
|
|
410
|
+
if job.task != self.opts.task:
|
|
411
|
+
raise BadRequest(f"this server loaded the {self.opts.task} pipeline; start another for {job.task}")
|
|
412
|
+
if float(job.fps) != int(job.fps):
|
|
413
|
+
raise BadRequest(f"fps must be a whole number for this muxer, got {job.fps:g}")
|
|
414
|
+
s, t = p.vae_spatial_compression_ratio, p.vae_temporal_compression_ratio
|
|
415
|
+
if job.width % 32 or job.height % 32:
|
|
416
|
+
raise BadRequest(f"width and height must be multiples of 32, got {job.width}x{job.height}")
|
|
417
|
+
if (job.num_frames - 1) % t:
|
|
418
|
+
raise BadRequest(f"num_frames must be {t}k+1, got {job.num_frames}")
|
|
419
|
+
shapes = {"video": (1, p.transformer.config.in_channels, (job.num_frames - 1) // t + 1, job.height // s,
|
|
420
|
+
job.width // s)}
|
|
421
|
+
avae = getattr(p, "audio_vae", None)
|
|
422
|
+
if avae is not None:
|
|
423
|
+
per_s = p.audio_sampling_rate / p.audio_hop_length / float(p.audio_vae_temporal_compression_ratio)
|
|
424
|
+
length = round(job.num_frames / job.fps * per_s)
|
|
425
|
+
shapes["audio"] = (1, avae.config.latent_channels, length,
|
|
426
|
+
avae.config.mel_bins // p.audio_vae_mel_compression_ratio)
|
|
427
|
+
return shapes
|
|
428
|
+
|
|
429
|
+
def _call_kwargs(self, job: ClipJob) -> dict:
|
|
430
|
+
torch = self.torch
|
|
431
|
+
kwargs = dict(prompt=job.prompt, negative_prompt=self.negative_prompt, height=job.height, width=job.width,
|
|
432
|
+
num_frames=job.num_frames, frame_rate=float(job.fps), num_inference_steps=job.steps,
|
|
433
|
+
guidance_scale=job.guidance_scale,
|
|
434
|
+
generator=torch.Generator(device="cpu").manual_seed(job.noise_seed),
|
|
435
|
+
output_type="np", return_dict=False, callback_on_step_end=self._on_step,
|
|
436
|
+
**self.call_kwargs)
|
|
437
|
+
if job.task == "i2v":
|
|
438
|
+
from PIL import Image
|
|
439
|
+
|
|
440
|
+
kwargs["image"] = Image.open(io.BytesIO(job.image)).convert("RGB")
|
|
441
|
+
return kwargs
|
|
442
|
+
|
|
443
|
+
def generate(self, job: ClipJob, noise: dict, clock: StageClock) -> Generated:
|
|
444
|
+
kwargs = self._call_kwargs(job)
|
|
445
|
+
rec, shapes = stepdump.StepRecorder(stepdump.request_of(job)), self.noise_shapes(job)
|
|
446
|
+
|
|
447
|
+
def record(stream, sched, step, model_output, timestep, sample, args, kw):
|
|
448
|
+
res = step(model_output, timestep, sample, *args, **kw)
|
|
449
|
+
i = sched.step_index - 1
|
|
450
|
+
rec.step(stream, sched.sigmas[i], sched.sigmas[i + 1], self._unpack(stream, sample, shapes),
|
|
451
|
+
self._unpack(stream, model_output, shapes), self._unpack(stream, _prev(res), shapes))
|
|
452
|
+
return res
|
|
453
|
+
|
|
454
|
+
self.clock, self._noise, self._noise_seen = clock, noise, {}
|
|
455
|
+
clock.set_steps(0) # the step callback counts from here: a pipeline that never calls it ran 0 steps
|
|
456
|
+
try:
|
|
457
|
+
with self._scheduler_steps(record):
|
|
458
|
+
video, audio = self.pipe(**kwargs)
|
|
459
|
+
finally:
|
|
460
|
+
self.clock = None
|
|
461
|
+
frames = video[0]
|
|
462
|
+
n, h, w = (int(d) for d in frames.shape[:3])
|
|
463
|
+
wave, rate = None, None
|
|
464
|
+
if audio is not None and job.audio is not False:
|
|
465
|
+
wave = audio[0].float().cpu()
|
|
466
|
+
rate = int(self.pipe.vocoder.config.output_sampling_rate)
|
|
467
|
+
expected = set(noise)
|
|
468
|
+
check = all(self._noise_seen.get(k) is True for k in expected) and set(self._noise_seen) >= expected
|
|
469
|
+
return Generated(video=frames, width=w, height=h, frames=n, fps=float(job.fps), audio=wave,
|
|
470
|
+
audio_sample_rate=rate,
|
|
471
|
+
effective={"noise_check": check, "noise_checked": sorted(self._noise_seen),
|
|
472
|
+
"generator": "cpu"}, step_dump=rec if rec.count else None)
|
|
473
|
+
|
|
474
|
+
# ------------------------------------------------------------------ per-step hooks (dumps and replay)
|
|
475
|
+
|
|
476
|
+
@contextmanager
|
|
477
|
+
def _scheduler_steps(self, handler):
|
|
478
|
+
"""For one pipeline call, every scheduler.step(model_output, timestep, sample, ...) goes to
|
|
479
|
+
``handler(stream, scheduler, step, model_output, timestep, sample, args, kwargs)``; ``step`` is the real
|
|
480
|
+
bound method. stream: "video" for pipe.scheduler, "audio" for any other instance (LTX-2's audio copy)."""
|
|
481
|
+
video = getattr(self.pipe, "scheduler", None)
|
|
482
|
+
if video is None: # nothing to hook (a stand-in pipeline without a scheduler)
|
|
483
|
+
yield
|
|
484
|
+
return
|
|
485
|
+
cls = type(video)
|
|
486
|
+
had, orig = "step" in cls.__dict__, cls.step
|
|
487
|
+
|
|
488
|
+
def step(sched, model_output, timestep, sample, *args, **kw):
|
|
489
|
+
bound = orig.__get__(sched, cls)
|
|
490
|
+
return handler("video" if sched is video else "audio", sched, bound, model_output, timestep, sample,
|
|
491
|
+
args, kw)
|
|
492
|
+
|
|
493
|
+
cls.step = step
|
|
494
|
+
try:
|
|
495
|
+
yield
|
|
496
|
+
finally:
|
|
497
|
+
if had:
|
|
498
|
+
cls.step = orig
|
|
499
|
+
else:
|
|
500
|
+
del cls.step
|
|
501
|
+
|
|
502
|
+
def _unpack(self, stream: str, t, shapes: dict):
|
|
503
|
+
"""A packed latent in the noise layout of ``shapes``; as it is when its size is not that shape's (a pipeline
|
|
504
|
+
that ran another shape than asked: the dump then shows that shape, which the checker refuses)."""
|
|
505
|
+
p, shape = self.pipe, shapes[stream]
|
|
506
|
+
if t.numel() != _numel(shape):
|
|
507
|
+
return t
|
|
508
|
+
if stream == "video":
|
|
509
|
+
return p._unpack_latents(t, shape[2], shape[3], shape[4], p.transformer_spatial_patch_size,
|
|
510
|
+
p.transformer_temporal_patch_size)
|
|
511
|
+
return p._unpack_audio_latents(t, shape[2], shape[3])
|
|
512
|
+
|
|
513
|
+
def _pack(self, stream: str, t):
|
|
514
|
+
p = self.pipe
|
|
515
|
+
if stream == "video":
|
|
516
|
+
return p._pack_latents(t, p.transformer_spatial_patch_size, p.transformer_temporal_patch_size)
|
|
517
|
+
return p._pack_audio_latents(t)
|
|
518
|
+
|
|
519
|
+
def _set_attention(self, name) -> None:
|
|
520
|
+
self.pipe.transformer.set_attention_backend(name)
|
|
521
|
+
connectors = getattr(self.pipe, "connectors", None)
|
|
522
|
+
if hasattr(connectors, "set_attention_backend"):
|
|
523
|
+
connectors.set_attention_backend(name)
|
|
524
|
+
|
|
525
|
+
def replay(self, job: ClipJob, dump, on_step, on_schedule=None, attention: str | None = None,
|
|
526
|
+
decode: bool = True) -> dict:
|
|
527
|
+
"""Run ``job`` through this pipeline with ``dump``'s latents forced in (see the module docstring).
|
|
528
|
+
|
|
529
|
+
``on_step(i, stream, sigma, sigma_next, ref_out, chain)``: the reference's guided output from the dump's x
|
|
530
|
+
at step i and the reference scheduler's update of the dump's output, both unpacked (on the device).
|
|
531
|
+
``on_schedule(sigmas)`` gets the reference's sigmas (N + 1 values) before the first update and may raise
|
|
532
|
+
to stop the replay. ``attention``: another backend for this replay only (the floor run); ``decode=False``
|
|
533
|
+
skips the decoders (frames None). Returns
|
|
534
|
+
{"frames": decoded frames [F, H, W, 3] in [0, 1], "initial": {stream: the reference's own initial latent},
|
|
535
|
+
"dtypes": {stream: {x, out, x_next: dtype name}}}."""
|
|
536
|
+
if attention and not self.opts.attention:
|
|
537
|
+
raise ValueError("a replay with another attention backend needs the pipeline's own backend set "
|
|
538
|
+
"(--attention, or reference mode), to switch back to it")
|
|
539
|
+
p, shapes, n = self.pipe, self.noise_shapes(job), dump.n_steps
|
|
540
|
+
initial, dtypes, counts = {}, {}, {}
|
|
541
|
+
|
|
542
|
+
def forced(stream, i, like):
|
|
543
|
+
key = f"step.{i}.{stream}.x" if i < n else f"final.{stream}"
|
|
544
|
+
return self._pack(stream, dump.get(key).to(like.device)).to(like.dtype)
|
|
545
|
+
|
|
546
|
+
def prepare(stream, fn):
|
|
547
|
+
def wrapper(*a, **kw):
|
|
548
|
+
out = fn(*a, **kw)
|
|
549
|
+
lat, rest = (out[0], out[1:]) if isinstance(out, tuple) else (out, None)
|
|
550
|
+
initial[stream] = self._unpack(stream, lat, shapes).float().cpu()
|
|
551
|
+
x = forced(stream, 0, lat)
|
|
552
|
+
return (x, *rest) if rest is not None else x
|
|
553
|
+
return wrapper
|
|
554
|
+
|
|
555
|
+
def step(stream, sched, real_step, model_output, timestep, sample, args, kw):
|
|
556
|
+
i = counts.get(stream, 0)
|
|
557
|
+
counts[stream] = i + 1
|
|
558
|
+
if i == 0 and stream == "video" and on_schedule is not None:
|
|
559
|
+
on_schedule([float(v) for v in sched.sigmas])
|
|
560
|
+
if i >= n: # the dump has fewer steps than the reference (on_schedule normally stopped it)
|
|
561
|
+
return real_step(model_output, timestep, sample, *args, **kw)
|
|
562
|
+
if stream not in dtypes:
|
|
563
|
+
dtypes[stream] = {"x": _torch_dtype(sample), "out": _torch_dtype(model_output)}
|
|
564
|
+
eng = self._pack(stream, dump.get(f"step.{i}.{stream}.out").to(sample.device)).to(model_output.dtype)
|
|
565
|
+
res = real_step(eng, timestep, sample, *args, **kw)
|
|
566
|
+
chain = _prev(res)
|
|
567
|
+
dtypes[stream].setdefault("x_next", _torch_dtype(chain))
|
|
568
|
+
j = sched.step_index - 1
|
|
569
|
+
on_step(i, stream, float(sched.sigmas[j]), float(sched.sigmas[j + 1]),
|
|
570
|
+
self._unpack(stream, model_output, shapes), self._unpack(stream, chain, shapes))
|
|
571
|
+
return _with_prev(res, forced(stream, i + 1, chain))
|
|
572
|
+
|
|
573
|
+
saved = {a: getattr(p, a) for a in ("prepare_latents", "prepare_audio_latents") if hasattr(p, a)}
|
|
574
|
+
for attr, stream in (("prepare_latents", "video"), ("prepare_audio_latents", "audio")):
|
|
575
|
+
if attr in saved and stream in shapes:
|
|
576
|
+
setattr(p, attr, prepare(stream, saved[attr]))
|
|
577
|
+
if attention:
|
|
578
|
+
self._set_attention(attention)
|
|
579
|
+
try:
|
|
580
|
+
kwargs = self._call_kwargs(job)
|
|
581
|
+
if not decode:
|
|
582
|
+
kwargs["output_type"] = "latent"
|
|
583
|
+
with self._scheduler_steps(step):
|
|
584
|
+
video, _audio = p(**kwargs)
|
|
585
|
+
finally:
|
|
586
|
+
for attr, fn in saved.items():
|
|
587
|
+
setattr(p, attr, fn)
|
|
588
|
+
if attention:
|
|
589
|
+
self._set_attention(self.opts.attention)
|
|
590
|
+
return {"frames": video[0] if decode else None, "initial": initial, "dtypes": dtypes}
|
|
591
|
+
|
|
592
|
+
def _on_step(self, pipe, i, t, callback_kwargs):
|
|
593
|
+
if self.clock is not None:
|
|
594
|
+
self.clock.step()
|
|
595
|
+
return {}
|
|
596
|
+
|
|
597
|
+
def write(self, out: Generated, path, clock: StageClock) -> None:
|
|
598
|
+
from diffusers.utils import encode_video
|
|
599
|
+
|
|
600
|
+
encode_video(out.video, fps=int(out.fps), output_path=str(path), audio=out.audio,
|
|
601
|
+
audio_sample_rate=out.audio_sample_rate)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The initial noise a clip starts from: drawn on the CPU from one seed, the same for every engine.
|
|
2
|
+
|
|
3
|
+
The recipe (an engine can follow it without inferencekit)::
|
|
4
|
+
|
|
5
|
+
g = torch.Generator(device="cpu").manual_seed(noise_seed)
|
|
6
|
+
for name, shape in shapes: # in order: "video" first, then "audio" when the model makes audio
|
|
7
|
+
noise[name] = torch.randn(shape, generator=g, dtype=torch.float32)
|
|
8
|
+
|
|
9
|
+
The numbers are drawn on the CPU and only then moved to the GPU. CUDA's generator gives other numbers for
|
|
10
|
+
the same seed, so noise drawn on the GPU would differ between engines (and is not promised to stay the
|
|
11
|
+
same across GPU models or library versions). ``noise_seed`` is the request's ``noise_seed``, or its
|
|
12
|
+
``seed`` when the request has none.
|
|
13
|
+
|
|
14
|
+
For LTX-2.x the shapes are the unpacked latents (diffusers_ltx.noise_shapes): video
|
|
15
|
+
[1, C, (frames - 1) // 8 + 1, height // 32, width // 32] and audio [1, Ca, L, M]. These are also the first
|
|
16
|
+
two draws a Diffusers LTX-2 pipeline makes when it is given a CPU generator seeded with ``noise_seed``.
|
|
17
|
+
|
|
18
|
+
``digest`` hashes names, shapes and the float32 bytes in order: equal digests mean identical noise. The
|
|
19
|
+
server reports it as ``effective.noise_sha256``, so two engines' records show whether they started from
|
|
20
|
+
the same tensors.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
|
|
27
|
+
RECIPE = "cpu-randn-float32/1" # bump if the recipe above ever changes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def initial_noise(noise_seed: int, shapes: dict) -> dict:
|
|
31
|
+
"""{name: CPU float32 tensor} drawn in the order of ``shapes`` from one CPU generator."""
|
|
32
|
+
import torch
|
|
33
|
+
|
|
34
|
+
g = torch.Generator(device="cpu").manual_seed(int(noise_seed))
|
|
35
|
+
return {name: torch.randn(tuple(int(d) for d in shape), generator=g, dtype=torch.float32)
|
|
36
|
+
for name, shape in shapes.items()}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def digest(noise: dict) -> str:
|
|
40
|
+
"""sha256 over (name, shape, float32 bytes) for each tensor, in order."""
|
|
41
|
+
h = hashlib.sha256(RECIPE.encode())
|
|
42
|
+
for name, t in noise.items():
|
|
43
|
+
t = t.detach().to("cpu").contiguous()
|
|
44
|
+
h.update(f"|{name}:{'x'.join(str(d) for d in t.shape)}|".encode())
|
|
45
|
+
h.update(t.numpy().tobytes())
|
|
46
|
+
return h.hexdigest()
|