File size: 12,019 Bytes
bd6bef3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# Base: torch~=2.7 is a real, hard dependency of ltx-core (its own
# pyproject.toml, not a guess). CUDA 13.0 chosen deliberately over the
# 12.6-based safer option -- Blackwell (96GB tier) support requires it, and
# torch==2.7.1 is confirmed (Qwen-Image-Edit-runpod's own real 2026-07-31
# deploy) as the first stable torch release with real sm_120 kernels. This
# reopens the "some RunPod hosts refuse CUDA>=12.8 containers" risk
# documented in ../BiRefNet-runpod/README.md and ../Qwen-Image-Edit-runpod/
# Dockerfile -- a deliberate, discussed tradeoff for this repo, not an
# oversight.
#
# ACTUAL RESOLVED VERSION, confirmed by a real local build + import test
# (2026-07-31, no GPU needed for this check): `uv sync --frozen` installs
# LTX-2's own uv.lock-pinned torch, which overrides whatever ships with this
# base image -- the real result is torch==2.9.1+cu128, NOT 2.7.1+cu1300 as
# this image's tag implies. Still satisfies ltx-core's `torch~=2.7`
# constraint (2.9.1 is within the 2.x series, >=2.7). If anything this is a
# BETTER outcome than planned: cu128 is exactly the build Qwen-Image-Edit-
# runpod already confirmed has working Blackwell/sm_120 kernels in a real
# deploy, not the untested cu130 this base image's own tag suggested.
FROM runpod/pytorch:1.0.3-cu1300-torch271-ubuntu2404
WORKDIR /app

# uv is LTX-2's own install method (uv sync --frozen from the repo root),
# not a pip package -- see LTX-2/README.md's quickstart. Installed via the
# official standalone installer rather than pip, matching upstream docs.
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"

# LTX v1.4.2: keep handler calls and all pipeline return formats at this pin.
ARG LTX2_COMMIT=9ec55f9f22798a3198d9c923856824821bc3317e
ENV LTX_UPSTREAM_COMMIT=${LTX2_COMMIT}
# Full clone (not --depth 1): the pin is a specific commit, not the branch
# tip, and a shallow clone would need its own --depth-1-of-that-commit
# fetch anyway (a second network round trip, not a saving) since the commit
# isn't known to git until it's fetched.
#
# The repo's own .gitattributes DOES configure git-lfs for several
# extensions (*.safetensors, *.png, *.mp4, test-asset *.wav, etc.) -- but
# the actual multi-GB model weights are NOT committed here; they live on
# Hugging Face (Lightricks/LTX-2.3, google/gemma-3-...) and are only
# referenced by path/URL from this repo's scripts. Confirmed by cloning it:
# the checkout is 5.0MB total. git-lfs is NOT installed in this image on
# purpose -- without it, any LFS-tracked file this repo does contain (small
# doc/test assets under packages/ltx-pipelines/tests/assets/, not anything
# this handler's code path touches) checks out as its plain-text LFS
# pointer instead of real content. Harmless here since nothing in
# ltx-core/ltx-pipelines' actual import path depends on those files, but
# flagged rather than silently assumed away.
RUN git clone https://github.com/Lightricks/LTX-2.git /opt/ltx2 \
 && cd /opt/ltx2 \
 && git checkout ${LTX2_COMMIT}

# `uv sync` WITHOUT --frozen, and that is a real regression to be aware of.
# The 2.3 build used `uv sync --frozen` against upstream's committed uv.lock.
# Upstream DELETED uv.lock in commit 2362161 ("Automated PR - 2026-08-11", the
# LTX-2.5 release), so --frozen now fails outright: there is no lockfile to
# freeze against. Verified by `git cat-file -e <pin>:uv.lock` at both pins.
#
# CONSEQUENCE: dependency resolution now happens at BUILD time, so two builds
# of the same LTX2_COMMIT can install different versions. To restore
# reproducibility, copy the uv.lock this build produces out of the image
# (/opt/ltx2/uv.lock), commit it to THIS repo, COPY it in before this step and
# put --frozen back. Deliberately not done pre-first-build: the resolution has
# to succeed once before it is worth pinning.
#
# `--extra natten` IS used, and skipping it was a real bug. The previous comment
# here claimed natten "only matters for the compiled attention path this handler
# does not enable" -- wrong on both counts, proven by a real job on 2026-08-25:
#
#   DiffVAE: natten is NOT installed. Falling back to a slower neighborhood-
#   attention backend (Triton if available, else pure-PyTorch tiled SDPA).
#   This path is for compatibility only -- install natten for production
#   DiffVAE decode
#   DiffVAE NA fallback: using Triton na3d.
#   -> RuntimeError: Triton Error [CUDA]: invalid argument
#
# natten is the DiffVAE VIDEO DECODER's neighborhood-attention backend, which
# EVERY job goes through. Without it the selection order is natten -> Triton ->
# eager SDPA, and the Triton na3d kernel fails at large frame counts: a 481-frame
# (20s) job generated fine through both denoising stages, peaked at 58.88 GiB,
# and then died in decode. 121 frames survives the fallback; 481 does not.
#
# And the build-risk half was wrong too: ltx-core's `natten` extra pins
# `natten==0.21.7+torch2130cu132` from https://whl.natten.org -- a PREBUILT wheel
# matching the torch 2.13.0+cu132 this image already resolves to. No CUDA compile,
# so the 30-minute build cap is not in play. Upstream's own README install line is
# exactly `uv sync --extra natten`.
#
# HEADS UP on torch: at this pin ltx-core pins torch==2.13.0 (cu132), a large
# jump from the 2.9.1+cu128 the 2.3 build actually resolved to. Upstream's own
# pyproject carries an override-dependencies entry forcing
# nvidia-cudnn-cu13==9.21.1.3, because the version torch pulls omits
# libcudnn_engines_tensor_ir and the soname then resolves against the system
# cuDNN, raising CUDNN_STATUS_SUBLIBRARY_VERSION_MISMATCH. That override comes
# along automatically with the workspace sync -- but it is written against
# THEIR CI image's system cuDNN, not this base image's. If the first build or
# first job dies with a cuDNN version mismatch, this comment is the reason.
# ltx-kernels (CUDA-compiled extensions) deliberately excluded from the
# default workspace by LTX-2's own pyproject.toml (see its
# `[tool.uv.workspace] exclude` comment) specifically so a plain `uv sync`
# never forces a CUDA toolchain at build time -- not opting into the
# `kernels` group here for that same reason, one fewer unverified variable.
#
# CONSEQUENCE, and it is the only thing gating the nvfp4 cost experiment:
# LTX_QUANTIZATION=nvfp4-cast / nvfp4-prequant need `ltx_kernels.nvfp4`, which
# lives in that excluded group. Enabling it means `uv sync --frozen --group
# kernels` here AND accepting a CUDA toolchain at build time (longer builds,
# and RunPod's GitHub integration caps a build at 30 minutes). Do that as a
# deliberate experiment -- with the nvfp4 transformer file already in the
# bundle -- not as a default.
WORKDIR /opt/ltx2
RUN uv sync --extra natten

# This project's own glue code (runpod SDK, GCS upload, image download) --
# kept separate from ltx-pipelines' own dependency set rather than editing
# their pyproject.toml/uv.lock, so `uv sync --frozen` above stays a clean,
# unmodified reproduction of upstream's own resolution.
COPY requirements.txt /app/requirements.txt
RUN uv pip install --python /opt/ltx2/.venv/bin/python -r /app/requirements.txt

# NOTE: do NOT bake the ~71GB of model weights into this image -- same
# reasoning as every other repo in this family (BiRefNet-runpod,
# Qwen-Image-Edit-runpod, Wan2.2-runpod): RunPod's own 80GB image-size cap
# (confirmed via RunPod's docs while building Qwen-Image-Edit-runpod) would be
# blown by this alone, before even counting the base image.
#
# SINGLE BUNDLE REPO STRATEGY. RunPod's cached-model feature ("Model" field)
# allows exactly ONE repo per endpoint ("Each endpoint is currently limited to
# one cached model at a time", docs.runpod.io/serverless/endpoints/model-caching)
# but provisions it UNBILLED ("You aren't billed for worker time while your
# model is being downloaded"). It also CANNOT filter which files it pulls (the
# same docs page: "the system currently downloads all quantization versions"),
# so naming Lightricks/LTX-2.5 in that field would pre-pull the whole 298.7GB
# repo -- including the *-comfy-int8-convrot.safetensors variants ltx-pipelines
# cannot even read.
#
# Fix: ONE user-owned HF "bundle" repo mirroring only what is needed, at
# IDENTICAL paths to upstream so handler.py's defaults work against either.
# Built 2026-08-24 with huggingface_hub.copy_files() -- server-side repo-to-repo
# copies, no bytes downloaded or uploaded, 140.75GB in ~11 seconds. Contents
# (see ../shoupapp/LTX_25_MIGRATION_PLAN.md section 3):
#   diffusion_models/ltx-2.5-22b-distilled-transformer-bf16.safetensors  42.02 GB  <- this handler
#   text_encoders/gemma4-12b-with-proj-ltx-2.5-bf16.safetensors          26.26 GB  <- this handler
#   vae/ltx-2.5-video-vae-bf16.safetensors                                1.47 GB  <- this handler
#   vae/ltx-2.5-audio-vae-bf16.safetensors                                0.36 GB  <- this handler
#   latent_upscale_models/ltx-2.5-latent-spatial-upscaler-x2-bf16-1.0...  1.00 GB  <- this handler
#   diffusion_models/ltx-2.5-22b-distilled-transformer-nvfp4.safetensors 18.72 GB  <- nvfp4 experiment
#   diffusion_models/ltx-2.5-22b-dev-transformer-bf16.safetensors        42.02 GB  <- 360deg/keyframe, later
#   loras/ltx-2.5-22b-distilled-lora-450-bf16.safetensors                 8.90 GB  <- 360deg/keyframe, later
# The last three are NOT loaded by this handler; they are staged so the
# keyframe-interpolation and premium-quality work needs no re-provisioning.
#
# !! REQUIRED CONSOLE SETTINGS !!
#   - "Model" field = the bundle repo id (tugrulv89/LTX_2.5-handler)
#   - Endpoint env var: LTX_MODEL_REPO=<same bundle repo id>
# handler.py resolves everything through the standard HF cache (HF_HUB_CACHE ->
# /runpod-volume/huggingface-cache/hub), which is exactly where the Model field
# pre-provisions files -- no local_dir= anywhere, so nothing is downloaded
# twice, and no Network Volume is required. Without the Model field the handler
# still works standalone -- it downloads ~71GB into that same cache, on BILLED
# time, on every cold start. Configure the field.
#
# The 2.3 trio (LTX_CHECKPOINT_REPO / LTX_GEMMA_REPO / LTX_GEMMA_SUBFOLDER) is
# GONE: 2.5's text encoder is a single file, not a Gemma snapshot directory, so
# there is no subfolder to point at. Per-file overrides are LTX_TRANSFORMER_FILE
# / LTX_TEXT_ENCODER_FILE / LTX_VIDEO_VAE_FILE / LTX_AUDIO_VAE_FILE /
# LTX_UPSCALER_FILE.
#
# !! HF_TOKEN !! required as a RunPod Secret while the bundle repo is private or
# the upstream repo is gated (Lightricks/LTX-2.5 is gated:auto -- the license
# must be accepted once on the model page).

COPY handler.py /app/handler.py

# DEV OVERRIDE (added 2026-07-31, after the 4th from-scratch GitHub build):
# if /runpod-volume/dev/handler.py exists, run IT instead of the baked-in
# /app/handler.py. Rationale: this endpoint deploys via RunPod's GitHub
# integration, which builds and redeploys on EVERY push to the watched
# branch (docs.runpod.io/serverless/workers/github-integration -- also the
# source of the 30-minute docker-build cap and 80GB image cap); its builder
# cache is not under our control, so in practice each handler-only tweak
# cost a full from-scratch image build. With this override, iterating on
# handler.py needs ZERO builds: copy the new file to the network volume
# (from any pod attached to it, e.g. `curl -o /runpod-volume/dev/handler.py
# <raw github url>`), then let the endpoint's workers restart. DELETE the
# file to return to the image's own pinned handler -- that is the
# production configuration; the override is a development mechanism.
# The echo makes an active override loudly visible in every worker log so
# it can't linger unnoticed.
CMD ["/bin/sh", "-c", "if [ -f /runpod-volume/dev/handler.py ]; then echo '!! DEV handler override active: /runpod-volume/dev/handler.py (delete it to use the baked-in handler)' && exec /opt/ltx2/.venv/bin/python -u /runpod-volume/dev/handler.py; else exec /opt/ltx2/.venv/bin/python -u /app/handler.py; fi"]