Видеомодель на 22 миллиарда параметров на RTX 3080: что пришлось сделать, чтобы она влезла в 10 ГБ
У меня RTX 3080 с 10 ГБ видеопамяти. У LTX-2.5 — 22 миллиарда параметров, 39 ГБ весов трансформера и ещё 24,5 ГБ текстового энкодера. По этим цифрам знакомство можно было бы закончить. Но модель удалось запустить локально под Windows и встроить в конвейер коротких роликов.
По дороге пришлось разобраться с загрузкой весов, освобождением памяти и одной неприятной ошибкой safetensors. А потом выяснилось, что большую часть времени компьютер вообще занят подготовкой модели: сам денойзинг занимает меньше 10 % времени. Расскажу, как это устроено и за что приходится платить, когда видеопамяти мало.
Что запускаем
Задача у меня вполне прикладная: собрать вертикальный ролик из 6–8 планов. Для этого на одном компьютере по очереди работают несколько моделей. Gemma через llama.cpp помогает со сценарием, FLUX.2 [klein] делает кадры с постоянным лицом героя, LTX-2.5 оживляет их. Дальше идут голос через Qwen3-TTS, музыка через ACE-Step и монтаж в ffmpeg.
В этой статье остановлюсь на LTX. Ей достаётся готовая картинка, которую нужно превратить в короткий видеоплан. Казалось бы, всего один шаг в длинной цепочке — но именно на нём пришлось повозиться с памятью.
Использую distilled-версию LTX-2.5. Видео получается в два этапа: сначала модель работает в низком разрешении, затем результат увеличивается и дорабатывается. По шагам это выглядит так:
8 шагов денойзинга в низком разрешении (~1,4 с на шаг),
латентный апскейлер ×2,
3 шага на полном разрешении 576×1024 (~5 с на шаг).
Время шагов здесь ориентировочное: это предварительные наблюдения, а не отдельный воспроизводимый бенчмарк.
Как 39 ГБ помещаются в 10 ГБ
Целиком модель в видеопамять, конечно, не помещается. Поэтому приходится уменьшать объём весов на GPU и подавать их туда частями. Для этого в штатном пайплайне из репозитория Lightricks/LTX-2 есть два ключа: --quantization fp8-cast и --offload cpu. Мой запуск выглядит так:
python -m ltx_pipelines.distilled \
--transformer-path ltx-2.5-22b-distilled-transformer-bf16.safetensors \
--text-encoder-path gemma4-12b-with-proj-ltx-2.5-bf16.safetensors \
--spatial-upsampler-path ltx-2.5-latent-spatial-upscaler-x2-bf16-1.0.safetensors \
--quantization fp8-cast --offload cpu \
--width 576 --height 1024 --num-frames 121 --frame-rate 24 \
--image first_frame.png 0 1.0 --prompt "..."
--quantization fp8-cast— веса хранятся в bf16, а при загрузке на GPU приводятся к fp8. Вдвое меньше памяти, отдельный fp8-чекпоинт не нужен.--offload cpu— трансформер собирается в «потоковом» режиме: блоки лежат в оперативной памяти и подаются на видеокарту по очереди, по мере счёта.PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True— без этого на длинных планах ловил OOM из-за фрагментации.
Память при этом никуда не исчезает из уравнения: нагрузка переезжает с видеокарты в обычную RAM. Для такой схемы нужно минимум 32 ГБ оперативной памяти, лучше 64, и файл подкачки от 32 ГБ. Так что одних слов «запускается на 10 ГБ» для описания требований недостаточно.
Грабли 1: safetensors падает на файлах больше 2 ГБ под Windows
До генерации первый запуск даже не добрался: всё закончилось access violation при открытии весов. Внятного сообщения о том, что именно не понравилось, не было.
В моём окружении проблема оказалась в чтении больших safetensors под Windows. На файлах больше примерно 2 ГБ библиотека пыталась отобразить storage целиком и падала. То есть разбираться пришлось сначала с тем, как прочитать модель с диска.
Обошёл это подменой safe_open: вместо штатного чтения используется read-only np.memmap. Заголовок файла разбираем сами, а тензоры отдаём как представления поверх отображённого файла. Это позволяет не расходовать commit-память на отдельную копию весов. Интерфейс оставляем прежним, поэтому для самого пайплайна LTX ничего не меняется.
Ниже — основа этой подмены. Это фрагмент для объяснения идеи, а не полный готовый патч:
class safe_open: # повторяет имя оригинала
def __init__(self, filename, framework="pt", device="cpu", **_):
with open(filename, "rb") as fh:
n = struct.unpack("<Q", fh.read(8))[0]
hdr = json.loads(fh.read(n))
self._meta = hdr.pop("__metadata__", None)
self._hdr, self._base = hdr, 8 + n
self._mm = np.memmap(filename, dtype=np.uint8, mode="r")
# keys(), metadata(), get_tensor(), get_slice() — поверх self._mm
Подключается без правки чужого кода: файл кладётся в site-packages окружения, а .pth-файл с import ltx_sft_patch подменяет модуль при старте интерпретатора.
Грабли 2: «выгрузить модель» не значит освободить память
Когда LTX заработала сама по себе, оставалось ужить её с остальными моделями. Видеокарта одна, а желающих занять её память несколько.
Сначала каждая модель жила в постоянном процессе-воркере: закончила работу — вызываем unload и передаём карту следующей. Но в моей схеме с CPU-offload после такой выгрузки часть видеопамяти и закреплённой RAM оставалась занятой. Следующая модель уже не помещалась, хотя предыдущую вроде бы выгрузили.
В итоге самым надёжным вариантом оказалось завершать процесс целиком. Каждая модель работает в своём процессе и своём venv, а после её шага процесс закрывается:
def release(self):
"""Освободить видеопамять под следующую модель: процесс завершается целиком."""
if self.proc is not None and self.proc.poll() is None:
try:
self.proc.stdin.close()
self.proc.wait(timeout=20)
except Exception:
self.proc.kill()
self.proc = None
У LTX отдельный процесс запускается вообще на каждый план. Это решило вопрос с освобождением памяти, но добавило ожидания — о нём дальше. А отдельные venv пригодились ещё и потому, что моделям нужны несовместимые версии torch и diffusers. В своих окружениях они хотя бы не мешают друг другу.
Где уходит время
Вот тут и обнаружилась цена постоянных перезапусков. По предварительным наблюдениям, на один план уходит около пяти минут. Из них сам денойзинг — примерно 25 секунд.
Большую часть времени приходится ждать чтения и подготовки примерно 65 ГБ весов: трансформера и Gemma-энкодера. И так для каждого следующего плана. Когда смотришь только на итоговое время, легко решить, что видеокарта просто медленно генерирует видео. Но по этим замерам основное ожидание приходится на подготовку.
Что из этого следует:
узкое место — диск и RAM, а не GPU. поэтому сначала стоит измерить скорость загрузки весов и использование памяти, прежде чем покупать более быструю видеокарту;
ролик из 6 планов — около получаса, короткий — несколько минут;
следующий шаг оптимизации — держать энкодер и трансформер в памяти между планами, а не перезапускать процесс. Этот вариант ещё предстоит проверить.
Как обходить слабости модели
После всех приключений с памятью остаётся ещё одна задача: получить движение, которое можно оставить в ролике. Здесь мне помогают несколько простых приёмов.
Стартовый кадр должен показывать состояние до действия. Если дверца уже открыта, а в промпте мы просим её открыть, модели приходится придумывать, что вообще должно происходить дальше. Лучше дать ей кадр, из которого нужное движение ещё возможно.
С руками сложнее. При контакте с предметом пальцы могут пройти сквозь ручку. Такое действие удобнее разнести на два плана: в первом рука тянется, затем склейка, а в следующем дверца уже открыта. Самый проблемный момент просто остаётся за кадром.
Если важно, чем закончится движение, конечный кадр можно передать вторым --image на последний кадр. Тогда у модели есть и начало, и точка, к которой нужно прийти.
Для липсинка схема отдельная: полная, не distilled-модель, distilled-LoRA и аудио-ветка a2vid_two_stage. Из результата беру только видео, а звук оставляю из озвучки.
Итог
В итоге LTX-2.5 работает на моей RTX 3080 и участвует в сборке роликов. Всё локально, без облака и подписок. Правда, за небольшой объём видеопамяти приходится платить оперативной памятью, временем загрузки и вознёй с окружением.
Для коротких роликов с этим уже можно работать. Следующая задача — сократить повторную загрузку весов между планами. А пока главный практический результат такой: 22B-модель на домашней карте с 10 ГБ оказалась вполне рабочим вариантом, со своими ограничениями.
Пример ролика: https://youtube.com/shorts/-jz5AJhnWxE Про режиссуру внутри конвейера подробно писал на DTF: https://dtf.ru/gamedev/5381388-kak-sozdat-video-s-pomoshchyu-rtx-3080-i-ltx-2-5 Портфолио: https://arybashov.github.io · канал: https://t.me/cg_pipeline_lab
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.