# 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 :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= # 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 # `), 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"]