Spaces:
Running on Zero
Running on Zero
Calibrate duration from three timed runs; set verified 44k token ceiling; document memory findings
Browse files
README.md
CHANGED
|
@@ -46,6 +46,7 @@ the transformer's own self-attention. Reference tokens ride along at `conditioni
|
|
| 46 |
| **Code** | `diffusers` main @ `d57cecde` — `LTX2InContextPipeline`, subclassed in `msr.py` |
|
| 47 |
| **Hardware** | ZeroGPU `xlarge` (96 GB). The 22B DiT plus the Gemma-4 encoder packs to ~71 GB, over the 48 GB slice |
|
| 48 |
| **Output** | 1280×704 @ 24 fps by default (1664×960 available), with LTX-2.5's native audio |
|
|
|
|
| 49 |
|
| 50 |
## Faithfulness to the reference implementation
|
| 51 |
|
|
@@ -69,8 +70,13 @@ The author's reference is a ComfyUI graph:
|
|
| 69 |
## Deviations
|
| 70 |
|
| 71 |
* **Resolution.** The workflow renders 1664×960 × 361 frames (15 s). The default here is
|
| 72 |
-
1280×704 × 97 frames (4 s)
|
| 73 |
-
workflow geometry and 8 s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
* **Tiled VAE decode.** The final decode is tiled (untiled it wants >100 GB). Reference
|
| 75 |
encoding is *not* tiled, matching the node's `use_tiled_encode=False`, with a tiled fallback
|
| 76 |
only if it would OOM.
|
|
|
|
| 46 |
| **Code** | `diffusers` main @ `d57cecde` — `LTX2InContextPipeline`, subclassed in `msr.py` |
|
| 47 |
| **Hardware** | ZeroGPU `xlarge` (96 GB). The 22B DiT plus the Gemma-4 encoder packs to ~71 GB, over the 48 GB slice |
|
| 48 |
| **Output** | 1280×704 @ 24 fps by default (1664×960 available), with LTX-2.5's native audio |
|
| 49 |
+
| **Memory** | `expandable_segments:True`. Stage 2 cycles ~1 GB activation buffers against a nearly full card; the default arena fragments and OOMs at 43k tokens where expandable segments complete in 87 s. Gemma and the connectors are released once each has produced its output |
|
| 50 |
|
| 51 |
## Faithfulness to the reference implementation
|
| 52 |
|
|
|
|
| 70 |
## Deviations
|
| 71 |
|
| 72 |
* **Resolution.** The workflow renders 1664×960 × 361 frames (15 s). The default here is
|
| 73 |
+
1280×704 × 97 frames (4 s), which runs in **53 s** so it fits comfortably in a visitor's
|
| 74 |
+
ZeroGPU quota; both the workflow geometry (1664×960 × 97, 87 s) and 8 s at 1280×704 (73 s)
|
| 75 |
+
are selectable under **Advanced**.
|
| 76 |
+
* **Length ceiling.** 76.2 GB stays resident during denoising and stage-2 activations cost
|
| 77 |
+
~0.33 MB per token, so the 96 GB slice tops out near 48k tokens. Combinations above 44,000
|
| 78 |
+
(`(latent_frames + slots × 5) × H/32 × W/32`) are refused up front instead of dying in an
|
| 79 |
+
OOM traceback — 15 s at the workflow resolution is simply out of reach here.
|
| 80 |
* **Tiled VAE decode.** The final decode is tiled (untiled it wants >100 GB). Reference
|
| 81 |
encoding is *not* tiled, matching the node's `use_tiled_encode=False`, with a tiled fallback
|
| 82 |
only if it would OOM.
|
app.py
CHANGED
|
@@ -69,10 +69,11 @@ DEFAULT_REFERENCE_FRAMES = 33 # the workflow's `reference_frames`
|
|
| 69 |
|
| 70 |
# Stage-2 sequence length is the memory driver, and here it is user-driven twice over
|
| 71 |
# (resolution x length, plus up to five reference slots each contributing a full clip of
|
| 72 |
-
# tokens). Measured on the 96 GB
|
| 73 |
-
#
|
| 74 |
-
#
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
print("[msr] downloading LTX-2.5 (diffusers)...", flush=True)
|
| 78 |
# `from_pretrained` derives allow-patterns from every model-like file in the repo, so it would
|
|
@@ -164,11 +165,6 @@ def _plan(resolution: str, seconds: float, reference_frames: int, num_refs: int)
|
|
| 164 |
return width, height, num_frames, ref_keep, tokens
|
| 165 |
|
| 166 |
|
| 167 |
-
# Reference point for both the cost model and the token ceiling: the default geometry with
|
| 168 |
-
# three reference slots, timed on the live Space.
|
| 169 |
-
REF_TOKENS = 24_640
|
| 170 |
-
REF_PIXELS = 1280 * 704 * 97
|
| 171 |
-
|
| 172 |
|
| 173 |
def _count_refs(*images) -> int:
|
| 174 |
return max(1, sum(1 for image in images if image is not None))
|
|
@@ -189,20 +185,24 @@ def gpu_duration(
|
|
| 189 |
):
|
| 190 |
"""Scale the ZeroGPU reservation with the work actually requested.
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
"""
|
| 198 |
num_refs = _count_refs(subject_1, subject_2, subject_3, subject_4, scene_image)
|
| 199 |
width, height, num_frames, _ref_keep, tokens = _plan(
|
| 200 |
resolution, seconds, reference_frames, num_refs
|
| 201 |
)
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
return int(min(200, math.ceil(1.15 * (fixed + decode + sample))))
|
| 206 |
|
| 207 |
|
| 208 |
def _write_video(frames, path: str, audio=None) -> None:
|
|
@@ -231,13 +231,16 @@ def _vram(label: str) -> None:
|
|
| 231 |
|
| 232 |
|
| 233 |
def _encode_once(prompt: str):
|
| 234 |
-
"""Run Gemma once
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
default geometry. `DiffusionPipeline.device` deliberately prefers a non-CPU component, so
|
| 241 |
`_execution_device` still reports cuda afterwards.
|
| 242 |
"""
|
| 243 |
pipe.text_encoder.to("cuda") # no-op on a fresh worker; restores it if one is reused
|
|
@@ -251,10 +254,8 @@ def _encode_once(prompt: str):
|
|
| 251 |
device=torch.device("cuda"),
|
| 252 |
)
|
| 253 |
finally:
|
| 254 |
-
_vram("before text-encoder eviction")
|
| 255 |
pipe.text_encoder.to("cpu")
|
| 256 |
_free()
|
| 257 |
-
_vram(f"after text-encoder eviction ({next(pipe.text_encoder.parameters()).device})")
|
| 258 |
return prompt_embeds, prompt_mask
|
| 259 |
|
| 260 |
|
|
@@ -340,7 +341,6 @@ def generate(
|
|
| 340 |
# One Gemma pass, reused by both stages, then evicted (see `_encode_once`).
|
| 341 |
torch.cuda.reset_peak_memory_stats()
|
| 342 |
prompt_embeds, prompt_mask = _encode_once(prompt)
|
| 343 |
-
_vram("weights resident")
|
| 344 |
|
| 345 |
shared = dict(
|
| 346 |
prompt=None,
|
|
|
|
| 69 |
|
| 70 |
# Stage-2 sequence length is the memory driver, and here it is user-driven twice over
|
| 71 |
# (resolution x length, plus up to five reference slots each contributing a full clip of
|
| 72 |
+
# tokens). Measured on the 96 GB slice: 76.2 GiB stays resident during denoising and peak
|
| 73 |
+
# activations run at 0.33 MiB/token (87.4 GiB peak at 35,200 tokens, 90.1 GiB at 43,680),
|
| 74 |
+
# which puts the wall just under 48k. 44,000 is the last value verified to complete; refuse
|
| 75 |
+
# past it rather than let the request die in an OOM traceback.
|
| 76 |
+
MAX_STAGE_2_TOKENS = 44_000
|
| 77 |
|
| 78 |
print("[msr] downloading LTX-2.5 (diffusers)...", flush=True)
|
| 79 |
# `from_pretrained` derives allow-patterns from every model-like file in the repo, so it would
|
|
|
|
| 165 |
return width, height, num_frames, ref_keep, tokens
|
| 166 |
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
def _count_refs(*images) -> int:
|
| 170 |
return max(1, sum(1 for image in images if image is not None))
|
|
|
|
| 185 |
):
|
| 186 |
"""Scale the ZeroGPU reservation with the work actually requested.
|
| 187 |
|
| 188 |
+
Least-squares fit through three timed runs on this Space — 53.1 s at 24,640 tokens /
|
| 189 |
+
87 Mpx, 72.8 s at 35,200 / 174 Mpx, 86.8 s at 43,680 / 155 Mpx — which land on a plain
|
| 190 |
+
linear model to within 0.2 s:
|
| 191 |
+
|
| 192 |
+
seconds = 9.5 + 0.0206 * megapixels + 1.697 * kilotokens
|
| 193 |
+
|
| 194 |
+
Megapixels cover the tiled VAE decode and the vocoder, kilotokens cover both sampling
|
| 195 |
+
stages, and the constant is the Gemma pass plus the mp4 mux. 15% margin on top and
|
| 196 |
+
nothing more: `duration` is charged against every visitor's daily quota, and a fat
|
| 197 |
+
reservation also costs queue priority.
|
| 198 |
"""
|
| 199 |
num_refs = _count_refs(subject_1, subject_2, subject_3, subject_4, scene_image)
|
| 200 |
width, height, num_frames, _ref_keep, tokens = _plan(
|
| 201 |
resolution, seconds, reference_frames, num_refs
|
| 202 |
)
|
| 203 |
+
megapixels = width * height * num_frames / 1e6
|
| 204 |
+
seconds_est = 9.5 + 0.0206 * megapixels + 1.697 * (tokens / 1000)
|
| 205 |
+
return int(min(140, math.ceil(1.15 * seconds_est)))
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
def _write_video(frames, path: str, audio=None) -> None:
|
|
|
|
| 231 |
|
| 232 |
|
| 233 |
def _encode_once(prompt: str):
|
| 234 |
+
"""Run Gemma once, then send it back to the host for the rest of the call.
|
| 235 |
+
|
| 236 |
+
Not CPU offloading: the text encoder is genuinely finished after this, and nothing moves
|
| 237 |
+
inside a sampling loop. Both `__call__`s below are handed the resulting `prompt_embeds`,
|
| 238 |
+
so `encode_prompt` short-circuits and never touches it again.
|
| 239 |
|
| 240 |
+
Measured effect is only ~1.9 GiB — ZeroGPU packs many parameters into shared CUDA
|
| 241 |
+
storages, so releasing one module's references frees far less than its nominal 24 GB —
|
| 242 |
+
but it is free and it lowers the peak the Gemma pass itself reaches.
|
| 243 |
+
`DiffusionPipeline.device` deliberately prefers a non-CPU component, so
|
|
|
|
| 244 |
`_execution_device` still reports cuda afterwards.
|
| 245 |
"""
|
| 246 |
pipe.text_encoder.to("cuda") # no-op on a fresh worker; restores it if one is reused
|
|
|
|
| 254 |
device=torch.device("cuda"),
|
| 255 |
)
|
| 256 |
finally:
|
|
|
|
| 257 |
pipe.text_encoder.to("cpu")
|
| 258 |
_free()
|
|
|
|
| 259 |
return prompt_embeds, prompt_mask
|
| 260 |
|
| 261 |
|
|
|
|
| 341 |
# One Gemma pass, reused by both stages, then evicted (see `_encode_once`).
|
| 342 |
torch.cuda.reset_peak_memory_stats()
|
| 343 |
prompt_embeds, prompt_mask = _encode_once(prompt)
|
|
|
|
| 344 |
|
| 345 |
shared = dict(
|
| 346 |
prompt=None,
|