Все статьи
обновлено 15 мин

Как я собрал на DGX Spark приватный AI-сервер, и теперь рассказываю что туда вошло

AGmind: 30 контейнеров, локальная 26B-модель, GPU-парсинг документов, drag-n-drop конструктор агентов в Dify. История сборки на DGX Spark GB10 с Dify + RAGFlow + vLLM, грабли драйверов и месяц работы с Claude Code.

dgx-sparkvllmragself-hosted-airagflowdify152-фзclaude-code

Оригинал статьи опубликован на habr.com . Эта версия — копия в нашем блоге.

DGX Spark — компактный AI-десктоп NVIDIA на чипе GB10 (128 ГБ unified memory, Blackwell GPU) размером с Mac mini, стартовая цена от $4 699. Помещает Llama 3.3 70B в FP8 без шардирования и подходит командам до 150 человек — тем, кому нужен self-hosted AI без datacenter-инфраструктуры. Кто не вписывается: крупные компании с 200+ concurrent-запросами.

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

Зачем мне понадобился свой AI-сервер

Облачные ассистенты (OpenAI, Anthropic, Gemini) хорошо работают с публичной информацией. Когда речь идёт о корпоративных документах, юридических договорах или внутренних регламентах, compliance-отделы справедливо отказывают SaaS-решениям. Дело не в TLS, а в том, что данные физически уходят на чужие серверы.

Доступные self-hosted альтернативы имели ограничения:

  • Open WebUI — приличный чат, но слабо работает с документами
  • Dify — мощный workflow-конструктор, но встроенный парсер PDF неудачный
  • RAGFlow — сильный парсер, но UI уступает Dify
  • Прочие инструменты — каждый имеет свои пробелы

Требование было простое: положить 200 PDF-документов в папку, задавать вопросы через нормальный чат, собирать агентов drag-n-drop, и чтобы всё поднималось одной командой.

Железо: что такое DGX Spark в двух словах

DGX Spark — компактный AI-десктоп от NVIDIA на чипе GB10 Grace Blackwell, анонсированный в марте 2025, продажи начались 15 октября 2025. Стартовая цена $3999, в феврале 2026 поднялась до $4699 из-за дефицита LPDDR5x.

Внутри один SoC:

  • 20-ядерный ARM-процессор (10 Cortex-X925 + 10 Cortex-A725)
  • Blackwell GPU: 48 SM, 6144 CUDA-ядра, 192 тензорных ядра пятого поколения
  • 128 ГБ памяти LPDDR5x (пропускная способность 273 ГБ/с)
  • Два QSFP-порта на 200 Гбит (возможность кластеризации)

По GPU-производительности это промежуточный вариант между RTX 5070 и 5070 Ti. Главное преимущество — 128 ГБ unified memory для инференса 70B-моделей в FP4 без шардирования. Среди альтернативного компактного железа с большой unified memory выделяется Apple Silicon — сравнение в статье «Mac Studio M3 Ultra для AI».

Проблема: драйверы. Spark стабилен на ветке 580.x. На 590+ обнаружены три регрессии:

  • vLLM зависает на CUDAGraph при первом инференсе
  • Утечка 80 ГБ памяти после завершения CUDA-процесса (требует перезагрузки)
  • TMA-баг в 595.58.03 ломает NVFP4-квантизацию

В install.sh поэтому зафиксирован драйвер 580: apt-mark hold nvidia-driver-580-open.

Архитектура: компоненты стека

Стек состоит из тридцати контейнеров.

Dify — фронтенд и оркестратор

Dify (langgenius/dify-api:1.13.3) — основной интерфейс, аналог LangChain Studio + ChatGPT-чат + drag-n-drop конструктор агентов. Workflow-редактор позволяет за пять минут собрать цепочку типа «получи документ → распарси → суммаризируй → отправь в Telegram» без кода.

Dify используется как primary frontend (agmind-dify.local). Состоит из пяти контейнеров: api, worker, web, sandbox, plugin_daemon.

RAGFlow — пересобранный под Spark

Upstream-RAGFlow не работает на DGX Spark: есть x86-only зависимости, устаревшие ONNX-рантаймы, неподдержка arm64 + sm_121 + CUDA 13.

Собран образ ar2r223/ragflow-spark:v0.24.1-spark на базе патчей HendrikSchoettle/ragflow-dgx-spark с собственными cherry-pick’ами:

Включено:

  • Cascade-OCR на Latin/Cyrillic/Chinese
  • File metadata в ES-чанках
  • Поддержка AVIF
  • ONNX Runtime GPU 1.25.0 под aarch64 + CUDA 13 + sm_121
  • TokenChunker для атомарных таблиц
  • TitleChunker с 5-уровневой иерархией
  • 7 ingestion-шаблонов (book, laws, manual, paper, resume и др.)
  • Русский prompt для vision-модели при language=Russian
  • Патчи бага Pipeline.globals

Размер образа ~13.3 ГБ.

vLLM — три инстанса

vLLM (LLM)vllm/vllm-openai:gemma4-cu130 с google/gemma-4-26B-A4B-it. NVIDIA playbook-сборка под arm64 и sm_121. gpu_memory_utilization=0.60 для запаса docling-serve.

vLLM (embeddings)nvcr.io/nvidia/vllm:26.02-py3 с deepvk/USER-bge-m3 (финтюненный под русский).

vLLM (reranker) — тот же базовый NGC-образ с BAAI/bge-reranker-v2-m3.

Выбор версии 26.02-py3 (а не 26.03) обусловлен требованием драйвера 595.45+, а система работает на 580.142.

Внимание-бэкенд зафиксирован на TRITON_ATTN: FlashInfer FP8 на sm_121 падает с «kernel only supports sm120» (известный баг).

Docling — GPU-парсер документов

Docling-serve (docling-serve-cu130:v1.16.1) — GPU-ускоренный конвертер от IBM. Принимает PDF/DOCX/PPTX/XLSX, выдаёт Markdown со структурой таблиц и описаниями картинок.

Три режима:

  • FAST — отключает OCR, ~4 сек на 5-страничной статье
  • BALANCED (default) — полный pipeline с автоматическим OCR
  • SCAN — для отсканированных страниц, easyocr + vision-описание через vLLM concurrency=8

Базы данных и хранение

  • PostgreSQL 16-alpine — метаданные Dify
  • Redis 7.4 — task queue, кэш, pub/sub
  • Weaviate 1.37 — векторное хранилище эмбеддингов
  • MinIO — S3-совместимое хранилище документов
  • Elasticsearch 9.x — чанки RAGFlow и полнотекстовый индекс
  • MySQL 8.0 — RAGFlow (захардкожена в схеме)

Мониторинг и операционка

  • Prometheus + Grafana 3001 с десятью дашбордами
  • На GB10 dcgm-exporter не работает; написан свой textfile collector для nvidia-smi
  • Loki + Grafana Alloy — логи всех контейнеров
  • Alertmanager — уведомления в Telegram
  • Portainer 2.39 — визуальное управление контейнерами и автодеплой на peer
  • fail2ban + UFW — firewall LAN-only по умолчанию

Безопасность

  • Секреты в /opt/agmind/credentials.txt (права 600, root-only)
  • 30+ Linux-capabilities сдропаны в каждом контейнере
  • Dify Sandbox изолирован в отдельной Docker-сети через Squid-прокси (SSRF-защита)
  • Опциональная Authelia с TOTP/WebAuthn
  • agmind rotate-secrets перегенерирует все пароли одной командой

Кластер из двух Spark’ов

Две машины соединяются QSFP 200G DAC в unified cluster:

┌─────────────────────┐                        ┌─────────────────────┐
│  spark-master       │   QSFP 200G DAC        │  spark-peer         │
│  (frontend + БД +   │ ◄──── direct link ────►│  (только vLLM,      │
│  Dify + RAGFlow +   │   192.168.100.0/24     │  выделенный GPU,    │
│  monitoring)        │                        │  128K context)      │
│  WAN: ethernet      │                        │  WAN: NAT через     │
│  iptables MASQUERADE│ ─────── default gw ───►│  master по QSFP     │
└─────────────────────┘                        └─────────────────────┘

Ключевые моменты:

  • На single-Spark vLLM делит GPU с docling (контекст ограничен 32K/64K/128K)
  • На dual-Spark peer крутит только vLLM, GPU выделен полностью, 128K без компромиссов
  • NAT-on-demand: peer выходит в WAN только при необходимости, потом отрезается
  • Симметричная установка определяет роль через LLDP, passwordless SSH настраивается автоматически

Что я делаю с этим в реальной жизни

Чат с локальной 26B-моделью:

  • TTFT 183 мс
  • 23–24 токена/сек на одиночном запросе
  • До 50 токенов агрегированно на трёх параллельных
  • Контекст 65K с FP8 KV-кэшем
  • До 45 параллельных запросов

Загрузка и обработка документов:

RAGFlow парсит PDF, вытягивает таблицы и картинки, прогоняет изображения через vision-модель (gemma-4 описывает их на русском), нарезает на чанки, загружает эмбеддинги в Weaviate. При вопросе bge-m3 ищет похожие чанки, bge-reranker сортирует, gemma-26B отвечает со ссылками на источник. Полная архитектура RAG для корпоративной базы знаний разобрана в «RAG для корпоративной базы знаний».

Конструктор агентов:

Собирается drag-n-drop в Dify. Пример: агент забирает письма из Gmail, классифицирует, важные — в Notion, остальные суммаризирует в дайджест и отправляет в Telegram. Собирается за десять минут без кода.

Бэкап: sudo agmind backup снимает Postgres, Redis, volumes и конфиги. Опционально с шифрованием age и upload на peer.

Производительность на бенчмарках

МетрикаРезультат
TTFT (streaming)183 мс
TPS (1 запрос)23–24 токенов/сек
TPS (3 параллельных)~50 токенов/сек агрегированно
Контекст65K (FP8 KV-cache)
Max concurrency @ 65K45 запросов
Память: веса48.5 GiB (bfloat16)
Память: KV-cache41.7 GiB (FP8)
Полный footprint~95 GiB

Docling (5-page arxiv PDF):

  • Cold start: 6.04 сек
  • Warm: 1.6 сек
  • Per-page (warm): 0.32 сек

284-page PDF без VLM: 88 сек Тот же PDF с VLM concurrency=8: 191 сек

Полный install.sh на чистой системе: ~30 минут (включая 52 ГБ весов)

Установка

git clone https://github.com/botAGI/AGmind.git
cd AGmind
sudo bash install.sh

Визард задаёт 10–15 вопросов: single/dual-Spark, выбор LLM, контекст, опциональные сервисы, место для бэкапов. Через ~25 минут стек готов, credentials в /opt/agmind/credentials.txt (права 600).

Day-2 операции:

agmind status              # сервисы, GPU, модели, эндпоинты
agmind doctor              # диагностика
agmind logs <service>      # тейл логов
agmind ragflow status      # RAGFlow контейнеры + ES health
agmind docling bench <pdf> # холодный/тёплый старт и per-page timing
agmind upgrade --diff      # что устарело
agmind backup              # Postgres + Redis + volumes
agmind rotate-secrets      # перегенерировать пароли и ключи

Пять грабель GB10

1. 502 на каждый запрос после force-recreate (потерял два дня)

Nginx-конфиг с upstream agmind_api { server api:5001; } кэшировал IP при старте. После docker compose up -d --force-recreate api новый контейнер получал новый IP, upstream-блок не обновлялся, nginx отвечал 502.

resolver влияет только на proxy_pass $variable, а не на upstream {}. Решение: переписать на variable form:

location /api {
    set $u_dify_api http://api:5001;
    proxy_pass $u_dify_api;
}

Убрать все upstream {}. Регрессионный тест проверяет, что после force-recreate сервис отвечает 200 без ручного перезапуска nginx.

2. Зомби-задачи в Redis после force-recreate (потерял два часа)

Большой PDF на 280 страниц запущен на docling — висит в processing десять минут. После docker compose up -d --force-recreate worker задача всё ещё processing.

Реальная причина: остатки Redis-state (ключи generate_task_belong:<task_id> с TTL 1800, pub/sub-каналы привязаны к старому hostname celery@OLDID). Recreate = новый контейнер с новым hostname, Redis об этом не знает.

Лечение:

redis-cli -a $PW -n 0 --scan --pattern 'generate_task_belong:*' | \
  xargs redis-cli -a $PW -n 0 DEL
redis-cli -a $PW -n 1 --scan --pattern 'celery-task-meta-*' | \
  xargs redis-cli -a $PW -n 1 DEL

FLUSHDB блокируется ACL. Правильный путь: менять env через docker restart, а не recreate.

3. mDNS self-collision (потерял час)

Попытка добавить три алиаса (agmind-dify.local, agmind-rag.local, agmind-storage.local) в /etc/avahi/hosts на один IP. Avahi пишет «Local name collision», алиасы не резолвятся.

Avahi регистрирует $(hostname).local → IP как primary record. Добавление алиаса на тот же IP видится как конфликт — probe в сеть не отправляется.

Фикс: использовать avahi-publish-address -R name.local IP через systemd-юнит вместо /etc/avahi/hosts.

Бонус-баг: второй mDNS-respondent (NoMachine, iTunes) на UDP/5353 ломает avahi молча. Проверка: sudo ss -ulnp | grep 5353.

4. Distroless-контейнеры без shell (потерял полчаса)

Loki, redis_exporter, nginx-prometheus-exporter — distroless-образы без /bin/sh. Healthcheck через CMD-SHELL wget ... выдаёт ошибку, контейнер висит unhealthy, хотя метрики отдаются.

Правило: для distroless-образов здоровье проверяется через Prometheus up{job=...}, а не через docker healthcheck.

5. APT и truncated downloads (потерял вечер)

apt-get install -y linux-firmware (603 МБ) падает с «неожиданный конец файла». SHA256 не совпадает, reinstall не помогает. Файл обрезается ~2 МБ до конца.

Curl по HTTP принимает truncated body как успех (HTTP 200, Connection closed до Content-Length). Где-то сидит прозрачный прокси/CDN, режущий соединение.

Решение: везде HTTPS. TLS требует close_notify alert, клиент знает, что body неполный. После замены http://ports.ubuntu.com на https:// всё качается чисто.

Месяц с Claude Code: что это на самом деле

Код не писался руками — все 88 КБ install.sh, 16 lib-модулей, nginx-шаблоны, Python-скрипты сгенерировал Claude Code (CLI-агент Anthropic).

Что работало хорошо:

  • Bash-скрипты с edge-case’ами (30 состояний хоста, частичная установка, существующие credentials, сломанный peer, занятый порт)
  • Регрессионные тесты под mock’и
  • Документация

Что работало плохо:

LLM врут, и это не лечится. Claude предложил minio/minio:RELEASE.2026-02-01T00-00-00Z — выглядит правдоподобно (timestamp правильного формата), но manifest unknown. После этого в CLAUDE.md написано: «Никаких выглядит правдоподобно. Только docker manifest inspect». Добавлен автотест проверки тэгов в registry.

Контекст забывается между сессиями. Без CLAUDE.md (200+ строк правил) ничего не работает. Каждое правило — выстраданное факапом.

Иногда не понимает приоритеты: просишь починить баг в OCR — попутно отрефакторит три соседних модуля.

Что я вынес про AI-разработку

AI-агент не делает тебя умнее. Он делает тебя быстрее в том, в чём ты уже разбираешься.

Без понимания, почему proxy_pass http://name ломается, а proxy_pass $variable — нет, Claude сгенерирует код, ты его поставишь, оно сломается, и ты не поймёшь почему.

Claude — не замена программисту. Это ускоренный младший разработчик для рутины, при условии, что архитектор есть. Где это реально меняет уравнение: в задачах с видением, опытом и предметной областью, но без команды и времени.

Раньше один человек мог сделать MVP за пару месяцев. Сейчас — production-grade продукт за 30 дней. В репозитории 86 коммитов, 32 тысячи строк, 30 контейнеров.

Дисциплина:

  • Чёткое видение продукта
  • CLAUDE.md с правилами, пополняется после каждого факапа
  • Живое железо для проверки

Итоговые ссылки

Вторая часть — про мониторинг unified memory без NVML и supply-chain hardening для Dify-плагинов в air-gap-сценариях — будет следующей.