Зачем пытаться запихнуть слона в холодильник?
Вам знакома эта ситуация: есть две крутые модели - GPT OSS для текста и Qwen VL для анализа изображений. И есть ваш скромный GPU с 6 ГБ памяти. В теории каждая модель требует 8-10 ГБ, но на практике вы хотите обе. Прямо сейчас. Без покупки новой видеокарты.
Стандартный подход "скачал-запустил" здесь не работает. Одна модель займет всю память, вторая не запустится. Можно переключаться между ними, но это медленно и неудобно. А что если нужно обработать текст и изображение в одном диалоге?
Попытка запустить обе модели одновременно стандартными методами закончится ошибкой CUDA out of memory. Это не баг - это физика. Но физику можно обмануть.
Что такое MCP-сервер и зачем он нам?
Model Context Protocol (MCP) - относительно новый стандарт от Anthropic для взаимодействия с моделями. По сути, это HTTP-сервер, который умеет управлять несколькими моделями, распределять запросы и память. Кастомный MCP-сервер - наш ключ к решению проблемы.
Вместо того чтобы грузить обе модели в память одновременно, мы сделаем умный диспетчер. Он будет держать одну модель активной, а вторую - в спящем состоянии с частичной выгрузкой. Когда понадобится вторая модель - первая выгружается, вторая загружается. Автоматически, без вашего участия.
Подготовка: что нужно перед началом
Прежде чем прыгать в код, убедитесь что у вас есть:
- NVIDIA GPU с 6 ГБ VRAM (RTX 2060, 3050, 3060 и подобные)
- Python 3.10 или новее
- CUDA 11.8 или 12.x (совместимая с вашим драйвером)
- Не менее 16 ГБ оперативной памяти
- 30 ГБ свободного места на диске для моделей
Проверьте установку CUDA:
nvidia-smiЕсли видите версию драйвера и свободную память - все хорошо. Если нет - сначала установите драйверы.
1Шаг 1: Выбор правильных версий моделей
Это самый критичный момент. Нельзя просто взять любые версии GPT OSS и Qwen VL. Нужны специально квантованные GGUF-версии.
Для GPT OSS ищем модель в формате GGUF с квантованием Q4_K_M или Q5_K_M. Почему именно эти?
- Q4_K_M дает хороший баланс качества и размера (~4-5 ГБ)
- Q5_K_M немного лучше по качеству, но больше (~5-6 ГБ)
- Q8 и выше не влезут вместе с Qwen VL
Для Qwen VL ситуация сложнее. Мультимодальные модели всегда больше. Ищем Qwen2-VL-7B-Instruct-GGUF с квантованием Q4_K_M. Да, только Q4. И только 7B версию. 14B или 32B не влезут даже поодиночке.
| Модель | Рекомендуемая версия | Размер | Где искать |
|---|---|---|---|
| GPT OSS | gpt-oss-7b-q4_k_m.gguf | ~4.2 ГБ | Hugging Face |
| Qwen VL | Qwen2-VL-7B-Instruct-q4_k_m.gguf | ~4.8 ГБ | Hugging Face |
Скачиваем обе модели в папку ~/.cache/gguf/ или другую удобную.
2Шаг 2: Установка llama.cpp с правильными флагами
Не устанавливайте llama.cpp из pip. Соберите из исходников с CUDA поддержкой:
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make clean
LLAMA_CUDA=1 make -j$(nproc)Проверяем что CUDA работает:
./main -m ~/.cache/gguf/gpt-oss-7b-q4_k_m.gguf -p "Hello" -n 10 --gpu-layers 20Если видите ответ и нет ошибок CUDA - все окей. Если получаете ошибку - проверьте переменные окружения:
export CUDA_VISIBLE_DEVICES=0
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATHТеперь главный трюк: определяем сколько слоев грузить на GPU. Для 6 ГБ и двух моделей:
# Для GPT OSS (4.2 ГБ модель)
./main -m gpt-oss-7b-q4_k_m.gguf -p "test" -n 5 --gpu-layers 18 --ctx-size 2048
# Для Qwen VL (4.8 ГБ модель)
./main -m Qwen2-VL-7B-Instruct-q4_k_m.gguf -p "test" -n 5 --gpu-layers 15 --ctx-size 1024Почему такие цифры? --gpu-layers определяет сколько слоев модели будут в VRAM. Остальные - в RAM. Меньше слоев на GPU = меньше потребление VRAM, но медленнее инференс. Экспериментируйте. Начните с 15-18 слоев для каждой модели.
Запустите каждую модель отдельно с разным количеством --gpu-layers и смотрите потребление VRAM через nvidia-smi. Цель - чтобы каждая модель занимала не более 3.5 ГБ VRAM в рабочем состоянии.
3Шаг 3: Создаем кастомный MCP-сервер
Теперь напишем сервер на Python, который будет управлять обеими моделями. Установите зависимости:
pip install fastapi uvicorn pydantic python-multipartСоздайте файл mcp_server.py:
import subprocess
import threading
import time
import json
import os
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import signal
app = FastAPI(title="Dual Model MCP Server")
class ModelManager:
def __init__(self):
self.current_model = None
self.process = None
self.models = {
"gpt_oss": {
"path": "/home/user/.cache/gguf/gpt-oss-7b-q4_k_m.gguf",
"gpu_layers": 18,
"ctx_size": 2048
},
"qwen_vl": {
"path": "/home/user/.cache/gguf/Qwen2-VL-7B-Instruct-q4_k_m.gguf",
"gpu_layers": 15,
"ctx_size": 1024
}
}
def switch_model(self, model_name: str):
"""Выгружаем текущую модель, загружаем новую"""
if self.current_model == model_name:
return True
# Останавливаем текущий процесс
if self.process:
self.process.terminate()
self.process.wait(timeout=5)
# Запускаем новую модель
model_config = self.models[model_name]
cmd = [
"./main",
"-m", model_config["path"],
"--gpu-layers", str(model_config["gpu_layers"]),
"--ctx-size", str(model_config["ctx_size"]),
"--simple-io",
"--n-predict", "512",
"--temp", "0.7"
]
self.process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.current_model = model_name
# Даем модели время на инициализацию
time.sleep(3)
return True
def generate(self, model_name: str, prompt: str) -> str:
self.switch_model(model_name)
if not self.process:
raise RuntimeError("Model not loaded")
# Отправляем промпт
self.process.stdin.write(prompt + "\n")
self.process.stdin.flush()
# Читаем ответ
output = []
while True:
line = self.process.stdout.readline()
if not line:
break
output.append(line.strip())
if "" in line or len(output) > 50:
break
return "\n".join(output)
manager = ModelManager()
class ChatRequest(BaseModel):
model: str
prompt: str
max_tokens: Optional[int] = 512
@app.post("/chat")
async def chat(request: ChatRequest):
try:
if request.model not in ["gpt_oss", "qwen_vl"]:
raise HTTPException(status_code=400, detail="Unknown model")
response = manager.generate(request.model, request.prompt)
return {"response": response, "model": request.model}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "ok", "current_model": manager.current_model}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)Это упрощенная версия. В реальном проекте добавьте обработку изображений для Qwen VL (она требует base64 кодирование), кэширование, таймауты и логирование.
4Шаг 4: Запуск и тестирование
Сначала запустите сервер:
cd llama.cpp
python mcp_server.pyВ другом терминале проверьте работу:
# Проверка здоровья
curl http://localhost:8000/health
# Запрос к GPT OSS
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"model": "gpt_oss", "prompt": "Explain quantum computing"}'
# Запрос к Qwen VL (текстовый режим)
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"model": "qwen_vl", "prompt": "Describe a sunset"}'Откройте nvidia-smi в третьем терминале и наблюдайте как память освобождается и занимается при переключении моделей.
Где собака зарыта: самые частые ошибки
Я видел десятки попыток сделать подобное. Вот что ломается чаще всего:
Ошибка 1: CUDA out of memory после нескольких запросов. Почему? llama.cpp не всегда полностью освобождает память при завершении. Решение: добавить --no-mmap флаг при запуске main, но это замедлит загрузку модели.
Ошибка 2: Модель загружается вечно. Проверьте --ctx-size. Слишком большой контекст (8192+) может не влезть даже в RAM. Начните с 1024-2048.
Ошибка 3: Qwen VL не понимает промпты. Мультимодальные модели требуют специального форматирования. Для текстовых запросов используйте стандартный чатовый формат, для изображений - base64 в JSON.
Ошибка 4: Сервер падает при параллельных запросах. Наш простой сервер не потокобезопасен. В продакшене используйте очередь запросов или запускайте отдельный процесс llama.cpp для каждого запроса.
А можно еще оптимизировать?
Конечно. Вот что дает дополнительный прирост:
- Используйте flash attention: соберите llama.cpp с
LLAMA_FLASH_ATTN=1. Ускоряет инференс на 20-30%. - Экспериментируйте с кэшированием K/V: флаг
--prompt-cacheи--prompt-cache-allускоряют повторные запросы. - Настройте swap: если не хватает RAM, добавьте swap файл 16-32 ГБ. Медленно, но лучше чем падение.
- Рассмотрите EXL2 формат: вместо GGUF. Более эффективное квантование, но сложнее в настройке.
Если у вас совсем старый GPU или мало RAM, посмотрите мою статью про CPU+RAM инференс. Там другие подходы.
А что насчет 8 ГБ или 12 ГБ VRAM?
С 8 ГБ можно позволить себе более высокое квантование (Q6_K) или больше слоев на GPU. С 12 ГБ - можно попробовать запустить 13B версии моделей, но осторожно.
Главное правило: оставляйте 1-2 ГБ VRAM про запас. Не только для самой модели, но и для активаций, контекста, системных нужд CUDA. Если nvidia-smi показывает 5.8/6.0 ГБ - ждите падения при следующем запросе.
Что дальше? Куда развивать этот проект
Рабочий MCP-сервер - только начало. Вот что можно добавить:
- Поддержку OpenAI-совместимого API: чтобы подключать стандартные клиенты.
- Веб-интерфейс: простой чат с переключателем моделей.
- Автоматическое определение типа запроса: текст -> GPT OSS, упоминание изображений -> Qwen VL.
- Кэширование моделей в RAM: не выгружать полностью, а держать веса в оперативке для быстрого переключения.
- Интеграцию с RAG: добавить векторную базу для документов.
Самое интересное - когда вы понимаете что эта архитектура масштабируется. Добавить третью модель? Просто расширите словарь models в менеджере. Больше памяти? Увеличьте --gpu-layers. Нужно обслуживать несколько пользователей? Добавьте балансировщик нагрузки.
И главное - теперь у вас есть полный контроль. Никаких ограничений по токенам, никаких цензурных фильтров (если только вы сами их не добавите), никакой отправки данных в облако. Только ваше железо, ваши модели, ваши правила.
Кстати, если вы думаете "это слишком сложно для 6 ГБ, проще купить карту на 24 ГБ" - вы правы. Проще. Но где в этом кайф? Где вызов? Где тот момент когда вы смотрите на nvidia-smi и видите как две модели уместились там, где по спецификациям не должна поместиться ни одна?
Вот ради этого мы и занимаемся локальным AI. Не потому что это дешево или удобно. А потому что можем.