Когда я собирал пробный ИИ-сервер на GTX 1050 Ti, мне удалось отдельно запустить и текстовую модель, и генерацию изображений. Но использовать их по отдельности и пользоваться ими через общий интерфейс — разные задачи.

У видеокарты было всего 4 ГБ памяти. Генерация картинки занимала почти весь этот объём. Оставлять рядом загруженную текстовую модель означало создавать конкуренцию за память.

Я решил не запускать вычисления одновременно. Вместо этого добавил небольшой промежуточный обработчик: он передаёт видеокарту одной задаче, освобождает память перед переключением и заставляет следующий запрос подождать.

Эта статья — о механизме, который я сделал для первоначальной конфигурации сервера с GTX 1050 Ti.

Почему недостаточно просто поставить две программы

Чат у меня обслуживает Ollama, изображения создаёт ComfyUI. Каждая программа управляет своими моделями, но сама по себе не координирует их с соседним приложением.

Завершение ответа тоже не обязательно означает освобождение видеопамяти. Загруженная модель может оставаться в памяти, чтобы следующий запрос выполнился быстрее.

Для одного приложения это удобно. Но на небольшой видеокарте сохранённая модель мешает другой задаче.

Поэтому мне требовалось решить сразу две вещи:

  • исключить одновременные вычисления;
  • перед сменой задачи выгружать модели предыдущего приложения.

Просто поставить запросы в очередь без освобождения памяти было бы недостаточно.

Где я разместил обработчик

Между веб-интерфейсом и API приложений я добавил собственную службу на Python с использованием aiohttp.

Схема получилась такой:

Запрос к чату → обработчик → Ollama
Запрос на картинку → обработчик → ComfyUI

Вычислительные запросы используют общую блокировку.

Внутренние интерфейсы обработчика слушают только loopback-адрес сервера. Открывать их наружу для этой задачи не потребовалось.

При этом я не стал блокировать абсолютно все обращения к приложениям. Получение состояния, просмотр результатов и другие служебные запросы должны работать и во время генерации.

Общую блокировку получают запросы, которые запускают вычисления: например, создание ответа в чате или отправка задания ComfyUI.

Что происходит перед запросом к чату

Когда приходит вычислительный запрос к Ollama, обработчик сначала ждёт освобождения общей блокировки.

После её получения он обращается к ComfyUI с просьбой выгрузить модели и освободить память. Затем проверяет состояние памяти, которое сообщает ComfyUI.

Я не ограничился отправкой команды «освободи». Если приложение не подтвердило освобождение в предусмотренный срок, обработчик возвращает ошибку вместо запуска конкурирующей задачи.

Только после этой проверки запрос передаётся текстовой модели.

Для посетителя это выглядит как обычный запрос к чату, хотя перед началом ответа серверу иногда приходится подготовить видеокарту.

Потоковый ответ тоже занимает видеокарту

Текстовая модель может отдавать ответ частями. Первые слова уже появились в браузере, но вычисление ещё продолжается.

Поэтому блокировка удерживается до завершения потока ответа, а не только до получения первых данных.

Я также предусмотрел случай, когда клиент отключился во время генерации. Обработчик перестаёт отправлять ему данные, но продолжает дочитывать ответ от Ollama.

Это нужно, чтобы не отдать видеокарту генератору изображений, пока текстовая модель ещё выполняет запрос.

Такое поведение не делает сервер неуязвимым к любым сбоям. Но обычное закрытие соединения не должно само по себе разрешать параллельный запуск второй задачи.

Перед картинкой я выгружаю текстовую модель

Для запроса на создание изображения действует та же общая блокировка.

Получив её, обработчик запрашивает у Ollama список загруженных моделей. Затем отправляет команды выгрузки с параметром:

{
  "keep_alive": 0
}

После этого он повторно проверяет список. Если модели всё ещё числятся загруженными, продолжать переключение нельзя.

Здесь есть различие между выключением службы и выгрузкой модели. Я не останавливаю Ollama перед каждой картинкой. Приложение продолжает работать, а модель освобождает занятую память.

Следующий запрос к чату сможет загрузить её снова.

Почему ответ ComfyUI ещё не означает готовую картинку

Самая существенная особенность оказалась в обработке изображений.

ComfyUI принимает задание и возвращает ответ раньше, чем заканчивается генерация. Поэтому нельзя освободить общую блокировку сразу после успешного ответа на запрос.

Иначе получится такая последовательность:

ComfyUI принял задание.
Обработчик решил, что работа закончена.
Чат получил видеокарту.
Картинка всё ещё генерируется.

Чтобы этого не происходило, после принятия задания обработчик запускает отдельное наблюдение за очередью ComfyUI.

Он проверяет и выполняемые, и ожидающие задания. Пока очередь не опустела, видеокарта остаётся занятой для чата.

После завершения очереди обработчик просит ComfyUI выгрузить модели, проверяет освобождение памяти и только затем снимает блокировку.

Что видит запрос, который пришёл во время генерации

Если картинка ещё создаётся, новый запрос к чату ждёт.

Я не прерываю изображение ради более позднего сообщения и не пытаюсь втиснуть обе задачи в память одновременно. Для первоначального домашнего сервера последовательная работа была понятнее.

Когда генерация завершается и ComfyUI освобождает память, запрос к чату получает возможность продолжить работу.

У такого решения есть цена: время ожидания включает не только сам ответ модели, но и завершение предыдущей задачи, выгрузку и повторную загрузку моделей.

Обработчик распределяет доступ к ограниченному ресурсу. Дополнительной видеопамяти он не создаёт.

Почему я сохранил WebSocket

Для работы ComfyUI недостаточно передавать только обычные HTTP-запросы. Интерфейс использует WebSocket для обмена сообщениями о ходе выполнения.

Я добавил двустороннюю передачу текстовых и бинарных сообщений между клиентом и приложением.

Само WebSocket-соединение не получает общую вычислительную блокировку. Оно должно оставаться доступным, пока генерация занимает видеокарту.

Таким образом, я разделил запуск работы и наблюдение за ней: первое требует исключительного доступа, второе продолжает работать параллельно.

Что происходит, если состояние очереди неизвестно

При ошибке запроса к очереди ComfyUI обработчик не считает видеокарту свободной.

Он записывает ошибку и повторяет проверку, сохраняя блокировку. Я выбрал осторожное поведение: лучше задержать следующий запрос, чем запустить его на основании неподтверждённого предположения, что предыдущая задача завершилась.

У этого решения есть обратная сторона. Если приложение долго не отвечает, ожидание тоже может затянуться. Для такой ситуации нужны проверка состояния и вмешательство, а не обещание, что очередь автоматически исправит любую неисправность.

В службе я предусмотрел простой статус: текущий режим работы и признак занятой видеокарты.

Как я проверил переключение

Я проверял механизм последовательностью реальных действий:

  1. Отправил запрос к текстовой модели и получил ответ.
  2. Запустил создание изображения.
  3. Пока изображение генерировалось, отправил следующий запрос к чату.
  4. Проверил, что этот запрос ждёт.
  5. Дождался картинки и последующего ответа текстовой модели.

В первоначальной проверке прошли также передача WebSocket, выгрузка Ollama перед изображением и освобождение памяти ComfyUI перед возвращением к чату.

После смены текстовой модели я повторил этот цикл. Отдельно проверил его через HTTPS после установки DreamShaper 8. По завершении обработчик вернулся в состояние idle, а признак занятости видеокарты стал ложным.

Короткие проверочные циклы я не использую как замер скорости обычной генерации. Их задача была другой: подтвердить порядок выполнения и работоспособность переключения.

Чем эта очередь не является

У меня получился небольшой координатор доступа к одной видеокарте, а не полноценная система фоновых заданий.

Ожидающие запросы находятся в памяти работающего обработчика. Я не добавлял постоянное хранилище очереди, сохранение заданий после перезапуска службы или гарантированное продолжение клиентского запроса после разрыва соединения.

Многопользовательскую нагрузку на первоначальной конфигурации я тоже не проверял.

Для моего сценария требовалось более узкое решение: пользоваться чатом и картинками через один интерфейс, не выгружая модели вручную при каждом переходе.

Что получилось

На первоначальном сервере с GTX 1050 Ti я проверил последовательную работу двух приложений на одной видеокарте. Чат дожидался завершения изображения, модели выгружались при переключении, а интерфейс продолжал получать сообщения о ходе работы.

Главной частью решения оказалась не сама блокировка. Нужно было правильно определить момент окончания задачи.

Для чата это завершение потока ответа. Для ComfyUI — опустевшая очередь и подтверждённое освобождение памяти. Если перепутать эти события с простым принятием запроса, очередь будет существовать только на бумаге.

В моём случае проверка полного цикла показала нужный результат: обе функции работают по очереди, и вручную управлять службами между запросами не требуется.