LTX_2.5-handler / handler /Dockerfile
tugrulv89's picture
Add pinned LTX v1.4.2 handler and validation provenance; adapters pending access
bd6bef3 verified
Raw History Blame Contribute Delete
12 kB
# 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"]