Утечка памяти в Ollama, vLLM, TGI: исправление через MALLOC_MMAP_THRESHOLD_ | AiManual
AiManual Logo Ai / Manual.
24 Мар 2026 Гайд

Ваш LLM сервер жрет память как не в себя? Виновник - фрагментация кучи glibc. Исправляем за 5 минут настройкой MALLOC_*

OOM-killed ваш сервер LLM? Это не утечка, а фрагментация кучи glibc. Глубокий разбор и пошаговое решение через настройку MALLOC_MMAP_THRESHOLD_ и MALLOC_TRIM_TH

Когда память утекает, но код чист

Вы ставите Ollama с свежей Llama 3.2 90B или разворачиваете vLLM для Qwen 2.5 72B. Все работает. Модель отвечает. Потом проходит час, два, десять запросов. И в самый неподходящий момент - хлоп. Killed. В логах ясно: Out of memory. Системный мониторинг показывает, что процесс съел всю доступную RAM и swap.

Первая мысль - утечка в Python, C++, в самом инференс-движке. Начинаешь искать memory_profiler, снимать дампы, писать баги в репозитории. Но проблема часто не в них. Она глубже. В самом фундаменте - в аллокаторе памяти стандартной библиотеки C (glibc), которую использует практически каждый сервер LLM, написанный на Python, C++ или Go.

Запомните: если память процесса в top или htop (столбец RES/VIRT) стабильно растет, но внутри процесса (например, через tracemalloc в Python) утечек не видно - это почти наверняка фрагментация кучи glibc.

Что ломается внутри malloc

glibc malloc (аллокатор ptmalloc2) - не волшебство. Он управляет двумя типами памяти: heap (куча) через системный вызов brk/sbrk и anonymous mapping (анонимные отображения) через mmap.

  • Куча (brk): один непрерывный регион памяти. Быстрый для мелких объектов. Но если вы выделяете и освобождаете объекты разного размера (а LLM серверы постоянно создают и удаляют буферы для токенов, промежуточных вычислений, KV-cache), эта куча превращается в швейцарский сыр. Свободные блоки памяти перемешаны с занятыми. Это фрагментация.
  • Mmap-регионы: каждый большой запрос памяти (по умолчанию > 128 KB) выделяется отдельным блоком через mmap. Когда такой блок освобождается, его можно сразу вернуть ядру. Фрагментации нет.

Проблема в пороге. По умолчанию glibc использует mmap для запросов больше 128 KB. А что делает LLM сервер? Он постоянно выделяет буферы для запросов, которые могут быть и 64 KB, и 100 KB. Они попадают в кучу. Фрагментируют ее. И самое главное - glibc крайне неохотно возвращает память из кучи обратно ядру.

💡
Аллокатор думает так: "Зачем возвращать память ядру, если процесс скоро снова попросит? Я просто оставлю эти свободные блоки у себя." Для долгоживущих серверных процессов, которые работают неделями, это логично. Но для LLM с их специфичным паттерном аллокаций (много средних буферов, которые живут недолго) - это смерть.

Два волшебных рычага: MMAP_THRESHOLD и TRIM_THRESHOLD

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

Переменная Значение по умолчанию Что делает
MALLOC_MMAP_THRESHOLD_ 128 KB (131072) Запросы памяти больше этого порога выделяются через mmap, а не из кучи.
MALLOC_TRIM_THRESHOLD_ 128 KB (131072) Если свободная память на верху кучи превышает этот порог, glibc попробует вернуть её ядру через madvise.

Стратегия проста: заставить glibc выделять больше памяти через mmap (где нет фрагментации) и заставить его чаще возвращать свободную память ядру.

1 Диагностика: точно ли это фрагментация кучи?

Перед тем как крутить ручки, убедитесь. Запустите ваш сервер LLM (Ollama, vLLM, Text Generation Inference) и дайте ему поработать под нагрузкой. Затем установите gdb и подключитесь к процессу (это безопасно для работы, если делать аккуратно).

# Находим PID процесса LLM сервера
ps aux | grep -E '(ollama|vllm|text-generation-launcher)'

# Подключаемся gdb (не забудьте установить debug symbols, если нужно)
gdb -p 

# В gdb выполняем команду для вывода статистики malloc
(gdb) call malloc_stats()

# Или, если хотите увидеть карту памяти процесса, выйдите из gdb (Ctrl+D) и:
cat /proc/<PID>/maps | head -30

В выводе malloc_stats() смотрите на строки system bytes (память, которую glibc получил от ядра) и in use bytes (память, которую использует ваша программа). Если первое сильно больше второго - у вас фрагментация. Иногда разница достигает 2-3x.

2 Эксперимент: настройка переменных окружения

Теперь самое простое. Останавливаем сервер. Выставляем переменные и запускаем заново. Значения подбираются экспериментально, но для большинства LLM серверов работает такая комбинация:

export MALLOC_MMAP_THRESHOLD_=131072  # 128 KB - можно оставить как есть или уменьшить
export MALLOC_TRIM_THRESHOLD_=131072   # 128 KB - оставляем

# Или более агрессивный вариант для сильно фрагментирующих нагрузок:
export MALLOC_MMAP_THRESHOLD_=65536    # 64 KB - больше аллокаций через mmap
export MALLOC_TRIM_THRESHOLD_=65536    # 64 KB - чаще тримим

Запускаем сервер с этими переменными. Для Ollama это будет выглядеть так:

MALLOC_MMAP_THRESHOLD_=65536 MALLOC_TRIM_THRESHOLD_=65536 ollama serve

Для vLLM или TGI добавьте переменные в команду запуска или в systemd service файл. Если вы сталкивались с проблемами vLLM на многопроцессорных системах, эта настройка может стать дополнительным стабилизирующим фактором.

Важно: не ставьте MALLOC_MMAP_THRESHOLD_ слишком низко (например, 4096). Каждое выделение через mmap создает отдельную VMA (Virtual Memory Area), что увеличивает нагрузку на ядро и может замедлить работу. Начните с 65536 (64 KB) и наблюдайте.

3 Мониторинг результатов: смотрим на графики

После применения настроек мониторинг обязателен. Смотрите не только на общее потребление памяти (RES), но и на:

  • Стабильность: память перестала бесконечно расти?
  • Производительность: не появились ли лаги? Выделение через mmap медленнее, чем через кучу.
  • Количество mmap-регионов: cat /proc/<PID>/maps | grep -c "^[0-9a-f].*\[anon\]"

Если память все еще утекает, возможно, проблема не только во фрагментации. Например, у вас может быть реальная утечка в коде, или ваш CPU инференс на старом сервере упирается в какие-то другие лимиты.

Ошибки, которые убьют ваш сервер быстрее OOM

С этими настройками можно настрелять себе в ногу. Вот как:

Ошибка Последствие Как избежать
Выставить MALLOC_TRIM_THRESHOLD_=1 glibc будет пытаться тримить память после освобождения КАЖДОГО байта. Катастрофа для производительности. Не опускайтесь ниже 4096. 65536 - безопасный порог.
Выставить MALLOC_MMAP_THRESHOLD_ в гигабайт Практически все аллокации пойдут в кучу. Фрагментация станет еще хуже, чем по умолчанию. Если хотите уменьшить mmap-аллокации, повышайте очень осторожно, не более 1-2 MB.
Использовать настройки в Docker без --privileged Некоторые версии glibc требуют прав для изменения этих параметров после запуска. Настройки просто проигнорируются. Устанавливайте переменные окружения при запуске контейнера: docker run -e MALLOC_MMAP_THRESHOLD_=65536 ...

А если не сработает? Альтернативные аллокаторы

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

  • jemalloc (от Facebook): разработан для многопоточных серверов с долгой жизнью. Значительно уменьшает фрагментацию. Установите libjemalloc2 и запускайте сервер с LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2.
  • tcmalloc (от Google): тоже хорош для многопоточности, но может вести себя нестабильно с некоторыми версиями Python. Если ваш LLM сервер написан на C++ (как llama.cpp), tcmalloc может дать прирост производительности.

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

Итог: когда это поможет, а когда нет

Настройка MALLOC_MMAP_THRESHOLD_ и MALLOC_TRIM_THRESHOLD_ - это не серебряная пуля. Она решит проблему, если:

  • Ваш сервер LLM постоянно выделяет и освобождает буферы среднего размера (от 4 KB до 128 KB).
  • Вы видите рост RES в top, но внутренние инструменты мониторинга (например, memory_profiler для Python) не показывают утечек.
  • Сервер убивается OOM-killer через несколько часов/дней работы, а не сразу.

Не поможет, если:

  • У вас реальная утечка в коде (например, не очищается кэш, растут глобальные списки).
  • Проблема в видеопамяти (VRAM), а не в оперативной. Тут нужно смотреть на квантование KV-кэша или другие техники.
  • Вы работаете на macOS (там другой аллокатор, и проблемы другие). Кстати, если вы маковод, почитайте про отключение сжатия памяти в macOS - это отдельная боль.

Последний совет, который почти никогда не пишут: после настройки этих переменных перезапустите сервер не один, а два раза. Иногда glibc кеширует некоторые структуры при первом запуске, и полный эффект виден только со второго. Это странно, но это работает.

И помните - если вы запускаете LLM на слабом железе, будь то домашний сервер с 10 ГБ VRAM или слабый Android, каждая мегабайта памяти на счету. Эти настройки могут быть разницей между стабильной работой и постоянными крашами.

Подписаться на канал