Daily MaverickTHE GATHERING 2026: Transforming The Wilds from a no-gone zone to a top rated parkESPN DeportesQuiñones anota para llegar a tres goles en Arabia; más actividad de mexicanosInquirerRidon, Diokno to present evidence on alleged Duterte unexplained wealthESPNGranddad, McConaughey and Arch Manning's dealings with fameRTP DesportoMundial2030. Mourinho gostava de final em Lisboa mas diz que deve ser em MadridThe Jerusalem PostNorway's Princess Astrid dies, aged 94, two days after king's funeralХабрКод как борьба. Кратчайшая история IT. 4. Кен Томпсон и Деннис РитчиScreen Rant8 Best New Witchy Books To Read After Seeing Practical Magic 2וואלהגבר בן 62 נפל מגובה במועצה אזורית באר טוביה - מצבו קשהRolling StoneVolunteers at the Gates of HellDeadlineUTA Signs ‘For All Mankind’s Toby KebbellAnime News NetworkVictoria of Many Faces Season 1 Anime Series Review
The Daily Newsstand · Free, Always
Friday, September 11, 2026

Что значит развернуть LLM локально: два дня с vLLM, DGX Spark и настоящими ограничениями

Translate

Когда меня однажды спросили, что такое инференс и как выглядит локальное развёртывание модели, я ответил правильно, но слишком общо: загрузить веса, поднять runtime, отдать API, подключить приложение.

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

Через несколько дней такой опыт появился.

За два дня нужно было поднять две локальные LLM на NVIDIA DGX Spark, развернуть отдельную модель распознавания речи на мини-ПК с AMD GPU, отдать всё через постоянные API и подключить к платформе AI-агентов «Искра» в контуре корпоративного заказчика.

Задача оказалась полезным напоминанием: «модель запустилась» и «AI-функция работает» — это совершенно разные уровни готовности. Между ними лежат совместимость железа и runtime, управление памятью, очереди, длинный контекст, потоковая выдача, tool calling, наблюдаемость и качество ответа на реальном маршруте.

В статье разберу этот путь именно как инженерный кейс: что пришлось проектировать, что измерять, какие оптимизации сработали, какую пришлось откатить и почему функциональная проверка на 256 тысяч токенов всё ещё не означает production-ready.

Все данные относятся к пилотному контуру. Заказчик обезличен. Результаты отдельных замеров нельзя механически переносить на другое железо, модели и пользовательские сценарии.

Исходная задача

В контуре уже была мультиагентная платформа «Искра». Модели требовалось разместить локально и подключить к ней три вычислительных сервиса:

  • основную LLM для сложных пользовательских запросов;

  • более быструю LLM для служебных и потенциально простых задач;

  • ASR-модель для транскрибации аудио и видео.

Получилась такая схема:

Пользователь
    │
    ▼
Искра: оркестрация агентов, история, инструменты
    │
    ├──► Основная LLM ──► DGX Spark №1
    │
    ├──► Fast LLM ──────► DGX Spark №2
    │
    └──► ASR API ───────► Beelink / AMD Radeon / WSL2

На двух Spark использовались:

  • Qwen3.8-27B-FP8 как основная модель;

  • Qwen3.6-35B-A3B-NVFP4 как fast-модель.

Для распознавания речи — Qwen3-ASR-1.7B на Radeon 8060S в WSL2.

Для LLM был выбран vLLM. Он умеет поднимать OpenAI-совместимый HTTP-сервер, поэтому приложение могло вызывать локальные модели через привычный /v1/chat/completions, не получая отдельную интеграцию под каждый runtime.

Это важное архитектурное решение: стандартизированный контракт отделяет приложение от конкретной реализации инференса. Модель, квантизацию или параметры сервера можно менять за API-границей, не переписывая весь агентный слой. Но совместимый endpoint не гарантирует совместимого поведения — особенно в streaming и tool calling. К этому ещё вернёмся.

День первый. Файлы модели — ещё не сервис

В бытовом разговоре «развернуть модель» часто звучит как «скачать её и запустить». На практике модель в репозитории — это набор артефактов:

  • веса и их формат;

  • конфигурация архитектуры;

  • токенизатор;

  • chat template;

  • иногда дополнительный код модели;

  • метаданные квантизации.

Сами по себе они не принимают HTTP-запросы, не ограничивают параллелизм, не проверяют ключи, не пишут полезные метрики и не восстанавливаются после перезагрузки.

Инференс в прикладном смысле — это выполнение уже обученной модели на входных данных. Но локальный inference service — больше, чем один вызов generate(). Его минимальный контур выглядит так:

артефакты модели
      +
совместимый runtime
      +
GPU-драйвер и вычислительный стек
      +
HTTP API / streaming / auth
      +
healthcheck / логи / restart policy
      =
сервис, который можно подключать к приложению

Первая инженерная проверка поэтому не «помещается ли число параметров в память», а вся матрица совместимости:

  1. архитектура CPU;

  2. поколение и возможности GPU;

  3. CUDA или ROCm и версия драйвера;

  4. поддержка конкретной квантизации;

  5. наличие нужных kernels в runtime;

  6. формат весов и chat template;

  7. архитектура контейнерного образа.

В нашем случае Spark — ARM64-система на Grace Blackwell. По спецификации NVIDIA, у неё 128 ГБ согласованной системной памяти, общей для CPU и GPU. Это позволяет работать с крупными моделями в компактном форм-факторе, но не отменяет конкуренцию за память между весами, KV-кэшем, runtime и остальными процессами.

ASR-контур был другим: AMD Radeon в WSL2 и ROCm. То есть одинаковая фраза «запустить модель на GPU» скрывала два разных стека, разные образы и разные способы диагностики. Для WSL2 у AMD есть отдельный путь установки и матрица совместимости; переносить CUDA-инструкции на такой узел бессмысленно.

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

С оговоркой по безопасности: встроенный API key в vLLM защищает не все возможные endpoint’ы. В официальной документации прямо рекомендуется учитывать это при публикации сервера и при необходимости ставить его за reverse proxy. «Есть ключ» и «поверхность сервиса закрыта» — не одно и то же.

Память: веса — только начало формулы

Следующая ловушка — оценивать потребление памяти только размером весов.

Для грубой инженерной модели полезнее держать в голове:

память процесса ≈ веса модели
                 + KV-кэш
                 + рабочие буферы и графы
                 + служебные структуры runtime
                 + память других процессов

Веса в основном определяются числом параметров и форматом хранения: FP8, FP4 и так далее. KV-кэш зависит уже от архитектуры модели, длины контекста, числа одновременно исполняемых последовательностей и его dtype. Рабочая память меняется с настройками prefill, CUDA graphs и конкретными kernels.

Именно поэтому вопрос «влезет ли модель?» неполон. Правильнее спрашивать:

  • с каким максимальным контекстом она влезет;

  • сколько запросов сможет исполнять одновременно;

  • какой запас останется системе;

  • не появятся ли вытеснения KV-кэша или swap;

  • что произойдёт при двух длинных запросах сразу.

Почему окно 256K пришлось оплачивать параллелизмом

Начальная конфигурация работала с окном 32K. Для реальных агентных сценариев этого было мало: системные инструкции, описания инструментов, история, документы и промежуточные результаты быстро съедают десятки тысяч токенов. Целью стало окно 256K — точнее, 262144 токена.

Обе LLM в итоге одновременно приняли по 259 616 входных токенов и нашли четыре из четырёх контрольных значений. Во время этой функциональной проверки не было OOM и аварийных перезапусков.

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

Это один из главных выводов кейса:

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

На основной модели совместный прогон 256K занял 510,32 секунды, на fast — 243,62 секунды. У основной было зарегистрировано семь вытеснений из KV-кэша, у fast — ни одного. Минимум доступной RAM во время пробы составлял 20,24 GiB, запись в swap — около 1,04 GiB.

vLLM при нехватке пространства KV-кэша может вытеснить запрос и позже пересчитать часть состояния. Это сохраняет работоспособность, но ухудшает end-to-end latency; такой эффект описан и в руководстве vLLM по оптимизации.

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

Скорость LLM — это не одно число

«Сколько токенов в секунду?» — полезный, но недостаточный вопрос. В пользовательском сценарии есть как минимум две разные фазы.

Prefill — обработка всего входного контекста. Она определяет значительную часть времени до первого токена, особенно на длинных запросах.

Decode — последовательная генерация ответа после prefill. Именно здесь обычно измеряют output tokens per second.

Для пользователя важны как минимум:

  • TTFT — time to first token;

  • скорость decode;

  • полное время сценария;

  • время в очереди;

  • время внешних инструментов;

  • стабильность streaming-выдачи.

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

CUDA graphs

CUDA graphs были включены на обеих чат-моделях. В коротких прогретых пробах получили:

Модель

До

После

Основная

7,48 токена/с

7,95 токена/с

Fast

26,63 токена/с

77,14 токена/с

Большой рост fast-модели нельзя целиком приписывать CUDA graphs: в той же серии для неё был увеличен бюджет памяти. Это не лабораторный A/B-тест с единственной переменной, поэтому корректная формулировка — «конфигурация стала быстрее», а не «CUDA graphs дали почти трёхкратное ускорение».

Multi-Token Prediction

Для основной модели сравнили варианты MTP 1, 2 и 3 и выбрали MTP3. В сопоставимой серии скорость выросла с 7,84 до 15,00 токена/с — примерно на 91%.

Но и этот результат относится к конкретному профилю генерации. Он не означает, что полный агентный сценарий стал быстрее на 91%: в нём остаются prefill, очередь, tool calls, сеть и логика приложения.

Prefill batch

Для основной модели batch оставили равным 2048: 4096 оказался медленнее, а 8192 не помещался в текущий бюджет вместе с контекстом 256K.

Для fast-модели значение 8192 оказалось полезным:

Входной контекст

TTFT до

TTFT после

47K токенов

10,62 с

8,40 с

128K токенов

41,85 с

34,17 с

Здесь хорошо видно, зачем отдельно измерять prefill. Пользователь может видеть высокую скорость генерации после первого токена и всё равно десятки секунд смотреть на пустой экран до его появления.

Быстрее — не всегда лучше

Естественная следующая идея: отправить простые и служебные задачи на fast-модель, а основную оставить для сложных запросов.

Архитектурно всё выглядело разумно:

запрос
  │
  ├── простой / служебный ──► fast LLM
  └── сложный ──────────────► main LLM

Fast действительно ускоряла часть инструментальных задач. Но трижды нарушила короткую инструкцию. Альтернативный режим основной модели без thinking, в свою очередь, не завершил один параллельный чат.

Оба варианта маршрутизации были отклонены, исходный backend восстановлен.

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

полезность = качество ответа
             × вероятность корректного действия
             × приемлемая задержка
             × эксплуатационная устойчивость

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

Интеграция проверяется не тестовым prompt’ом

После появления OpenAI-совместимого endpoint’а легко решить, что основная работа завершена. Для обычного чата этого иногда достаточно. Для агента — нет.

Агентный контур добавляет:

  • streaming событий;

  • структурированные tool calls;

  • схемы аргументов;

  • повторные ходы после выполнения инструмента;

  • историю и системные инструкции;

  • обработку ошибок инструмента;

  • защиту от нежелательных побочных эффектов.

В одном коротком реальном чате агент сделал непрошенный вызов календаря, после чего отменил созданное событие. Срабатываний события не было, но сам выбор действия был дефектом. Такой случай не обнаружит healthcheck и почти наверняка не покажет одиночный запрос «напиши стихотворение».

В другом тесте модель неправильно записала словами число 10 555 — «десять тысяч пятьсот пять», а строкой ниже фактически противоречила собственному ответу. Причинную связь с MTP, квантизацией или контекстом мы не устанавливали: для этого нужен отдельный эксперимент, где меняется один фактор за раз.

Здесь важно не соблазниться удобным объяснением. После настройки производительности любая новая ошибка кажется следствием последней оптимизации. Но временная близость — ещё не причинность.

ASR: быстрый сервис не гарантирует быстрый пользовательский ход

На отдельном узле был поднят постоянный ASR-сервис. В прогретых синтетических пробах:

  • 20,9 секунды аудио распознавались за 1,9 секунды;

  • 83,6 секунды — за 9,9 секунды.

Были проверены WAV, WebM/Opus и M4A/AAC, а также тишина и повреждённые файлы. Но длинное видео выявило ограничение: попытка отправлять крупные части приводила к отказу ASR.

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

Видео длиной 28 минут 55 секунд было обработано за 150,96 секунды: 31 из 31 частей, 18 223 символа результата.

Это подтверждает прохождение и сохранение всех частей, но не полную точность транскрипта. Прослушивание всего материала, качество имён, повторы на стыках, таймкоды и разделение спикеров оставались отдельной приёмкой.

Более показателен пользовательский запрос с аудио длиной 21 секунду. Сам вызов transcribe_file занял 2,03 секунды и правильно извлёк время встречи, три темы и контрольное число. Полный агентный ход занял 106,41 секунды.

Почему? После ASR модель ещё должна была обработать накопленный контекст, спланировать действие, вызвать инструмент и сформировать ответ. В двух совсем коротких чатах каждый запрос к основной LLM содержал 46 697 входных токенов.

Отсюда ещё один практический вывод:

Быстрый компонент не гарантирует быстрый сценарий. Измерять нужно критический путь пользователя целиком.

Контекст оказался отдельным архитектурным долгом

46,7K токенов для короткой реплики — тревожный сигнал. В эту сумму могли входить системные инструкции, схемы инструментов, подключённые навыки, история и пользовательские данные. На момент завершения работ точная разбивка ещё не была снята.

Просто увеличить окно с 32K до 256K в такой ситуации недостаточно. Большое окно откладывает ошибку переполнения, но одновременно:

  • увеличивает TTFT;

  • съедает KV-кэш;

  • уменьшает возможный параллелизм;

  • повышает стоимость каждого лишнего блока инструкций;

  • делает очередь заметнее для пользователя.

Следующий разумный этап — ввести budget контекста и наблюдаемость по его составу:

входной контекст
    ├── system prompt
    ├── tool schemas
    ├── skills / policies
    ├── история
    ├── документы пользователя
    └── результаты инструментов

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

Это уже не настройка vLLM. Это архитектура агентного приложения.

Инцидент, который нельзя автоматически назвать OOM

После основных работ один из Spark завис. При высоком давлении на unified memory такая версия выглядит правдоподобно, особенно после длинных контекстов, KV-cache preemption и записи в swap.

Но в сохранённых данных не было достаточного подтверждения, что причиной был именно OOM. Поэтому корректная запись инцидента звучит не «модель положила сервер по памяти», а:

узел завис; OOM является гипотезой, причина не подтверждена.

Разница кажется бюрократической только до первой неверной оптимизации. Если причина — драйвер, kernel, питание или другой процесс, уменьшение контекста может замаскировать симптом, но не устранить дефект.

Именно на этом проходит граница между пилотом и production. В пилоте можно доказать, что система функционально работает. Для production нужны воспроизводимость, мониторинг, алерты, управляемое восстановление, нагрузочная модель и подтверждённые SLO.

Что в итоге было готово, а что — ещё нет

Зафиксированная рабочая конфигурация выглядела так:

Компонент

Конфигурация

Основная LLM

Qwen3.8-27B-FP8, контекст 262 144, CUDA graphs, MTP3, prefill batch 2048, одна активная последовательность

Fast LLM

Qwen3.6-35B-A3B-NVFP4, контекст 262 144, CUDA graphs, prefill batch 8192, одна активная последовательность

ASR

Qwen3-ASR-1.7B, GPU/BF16, постоянный API, чанки 60 секунд с перекрытием 3 секунды

Интеграция

OpenAI-совместимые API, streaming, auth, healthcheck, вызовы из Искры

Что было подтверждено:

  • воспроизводимый запуск сервисов;

  • подключение к агентной платформе;

  • функциональный длинный контекст на обеих LLM;

  • распознавание нескольких аудиоформатов и длинного видео;

  • измеримые улучшения отдельных участков inference path;

  • управляемый откат неудачной маршрутизации.

Что нельзя было честно объявить готовым production:

  • целевые latency и concurrency ещё не были согласованы;

  • один активный запрос создавал очередь;

  • качество длинного транскрипта не прошло полную приёмку;

  • причины неправильных ответов и нежелательных действий не локализованы;

  • состав 46,7K обязательного контекста не разобран;

  • причина зависания узла не подтверждена;

  • часть изменения нарезки оставалась пилотной поставкой.

Формулировка «production-oriented пилот» здесь точнее, чем «production-ready».

Как я теперь отвечаю на вопрос «что значит развернуть модель локально»

Локально развернуть модель — значит не просто загрузить веса на GPU.

Это значит:

  1. подобрать совместимые железо, драйвер, runtime, формат и квантизацию;

  2. превратить артефакты модели в постоянный сервис с понятным API;

  3. рассчитать бюджет памяти для весов, KV-кэша и рабочих буферов;

  4. принять явный компромисс между контекстом, параллелизмом и задержкой;

  5. проверить prefill, TTFT, decode и полный пользовательский сценарий отдельно;

  6. убедиться, что streaming и tool calling корректны именно для агентного приложения;

  7. измерить качество после каждой оптимизации, а не только tokens per second;

  8. добавить healthcheck, логи, метрики, restart policy и сценарии деградации;

  9. отличать успешный запуск, рабочий пилот и доказанную production-устойчивость.

Теперь за словами «поднять локальный инференс» для меня стоят не общие фразы, а вполне физические вещи: минуты ожидания первого токена, давление KV-кэша, запросы в очереди, 31 аудиочанк, откат быстрой модели из-за качества и инцидент с неподтверждённой причиной.

И, пожалуй, главный вывод такой: локальный inference — это не свойство модели. Это свойство всей системы вокруг неё.

Чек-лист перед следующим локальным развёртыванием

  • [ ] Зафиксированы архитектура CPU/GPU, драйвер и версии runtime.

  • [ ] Проверена поддержка модели, dtype и квантизации.

  • [ ] Разведены веса, KV-кэш и системный запас памяти.

  • [ ] Определены максимальный контекст и реальный параллелизм.

  • [ ] Измерены TTFT, decode, очередь и end-to-end latency.

  • [ ] Проверены OpenAI-совместимость, streaming и tool calling.

  • [ ] Есть auth не только «для галочки», закрыта вся поверхность API.

  • [ ] Есть healthcheck, restart policy, логи и метрики.

  • [ ] Качество проверяется на реальных сценариях после каждой оптимизации.

  • [ ] Побочные эффекты инструментов тестируются в безопасном режиме.

  • [ ] Для длинного аудио определены chunking, overlap и сборка результата.

  • [ ] Отдельно записано, что доказал пилот и чего он ещё не доказал.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.