Оглавление
- Когда лимит памяти маскируется под проблему GPU
- Что меняет pinned memory — и чего она не исправляет
- Baseline: увидеть лимит, права и реально locked страницы
- Docker и Compose: конечный ulimit вместо лишней capability
- systemd: закрепить политику в unit и проверить effective state
- vLLM offload: где pinned memory становится частью data path
- Application gate: проверить PyTorch transfer и сам inference
- Наблюдаемость: связать VmLck с latency и memory pressure
- Rollout: два gate, canary и контрфактическая проверка
- Типичные ошибки: unlimited, один benchmark и неверный слой
- Итог: memlock — это бюджет, а не переключатель скорости
Готовы перейти на современную серверную инфраструктуру?
В King Servers мы предлагаем серверы как на AMD EPYC, так и на Intel Xeon, с гибкими конфигурациями под любые задачи — от виртуализации и веб-хостинга до S3-хранилищ и кластеров хранения данных.
- S3-совместимое хранилище для резервных копий
- Панель управления, API, масштабируемость
- Поддержку 24/7 и помощь в выборе конфигурации
Результат регистрации
...
Создайте аккаунт
Быстрая регистрация для доступа к инфраструктуре
Когда лимит памяти маскируется под проблему GPU
Сервис LLM запускается без ошибок, GPU виден, модель помещается в видеопамять — но первый запрос после рестарта идёт рывками, CPU загружен копированием, а режим offload внезапно становится медленнее. Типичная реакция — искать неисправность в CUDA, PCIe или драйвере. Однако причина нередко находится выше: процесс не может закрепить нужный объём host RAM, потому что его RLIMIT_MEMLOCK остался на маленьком значении.
Pinned, или page-locked, memory — это страницы оперативной памяти, которые ядро не может выгрузить или переместить обычным способом. CUDA использует такой буфер для предсказуемых DMA-передач и асинхронного копирования между CPU и GPU. Официальная CUDA Programming Guide прямо указывает: page-locked память нужна для asynchronous copies, а cudaHostRegister() позволяет закрепить уже выделенный диапазон.
Но сама возможность pinning ещё не доказывает, что приложение стало быстрее. В проде нужны два независимых ответа. Infrastructure gate подтверждает лимит, права и реальный объём locked memory. Application gate показывает, что изменились полезные метрики: время холодного старта, TTFT, p95 или скорость offload-пути. Если второй ответ отрицательный, открывать memlock дальше бессмысленно: узкое место уже в storage, NUMA, PCIe, CPU preprocessing или очереди запросов.
Материал посвящён именно этой границе. Он не заменяет диагностику NVIDIA Xid и не обещает универсального ускорения. Задача практичнее: сделать pinned memory измеряемым ресурсом, задать конечный бюджет в Docker или systemd и получить воспроизводимый rollback.

Что меняет pinned memory — и чего она не исправляет
Обычная pageable memory удобна ядру: страницы можно перемещать и при необходимости отправлять в swap. Для передачи на дискретный GPU драйверу часто нужен промежуточный закреплённый буфер. В результате логический путь «тензор из RAM в GPU» может включать дополнительное копирование и синхронизацию. Pinned memory убирает эту неопределённость: физические страницы остаются на месте, и устройство может использовать стабильный DMA-адрес.
Это особенно важно, когда приложение хочет перекрыть копирование вычислением через cudaMemcpyAsync(). В CUDA Best Practices Guide NVIDIA связывает асинхронные transfers с page-locked host memory, но одновременно предупреждает: pinning — тяжёлая операция, а чрезмерный объём закреплённых страниц способен ухудшить работу всей системы. Поэтому «unlimited» не является тюнингом. Это отказ от бюджетирования дефицитного ресурса.
Представьте сервер с несколькими inference-worker. Один закрепляет весовые буферы, второй — CPU KV cache, третий готовит мультимодальные входы. Каждый по отдельности выглядит разумно, но вместе они отнимают у ядра свободу reclaim. При всплеске file cache или sidecar-нагрузки возникает memory pressure, хотя free ещё показывает доступную RAM. Проверяли ли вы, сколько страниц реально находится в VmLck, а не сколько памяти процесс просто зарезервировал?
Pinned memory не ускоряет GPU kernels, не исправляет медленный NVMe и не увеличивает пропускную способность PCIe. Она улучшает конкретный участок host↔device transfer. Если модель целиком живёт в VRAM после старта и запросы почти не двигают данные через CPU, прирост может оказаться незаметным. Поэтому сначала формулируйте гипотезу: какой буфер закрепляется, в какой фазе он копируется и какая прикладная метрика должна отреагировать.

Baseline: увидеть лимит, права и реально locked страницы
Начните до изменения unit-файлов и контейнерных параметров. Снимите soft и hard limit из того же окружения, где работает inference-процесс. Значение в вашей SSH-сессии не доказывает значение внутри Docker, Podman или systemd service: каждый launcher может установить собственный rlimit. Linux определяет RLIMIT_MEMLOCK как максимальное число байтов, которое непривилегированный процесс может закрепить в RAM; лимит округляется по размеру страницы. Это поведение описано в актуальной странице getrlimit(2).
Следующий набор команд не меняет систему. Он показывает лимит текущей оболочки, лимит конкретного PID и фактический объём locked pages. Запускайте его на canary-узле под тем же пользователем и через тот же launcher, что и сервис.
set -euo pipefail
ulimit -Sl
ulimit -Hl
grep -i "Max locked memory" /proc/self/limits
pid="$(pgrep -n -f 'vllm|python.*serve')"
grep -E '^(Name|VmRSS|VmLck|VmSwap):' "/proc/$pid/status"
grep -E '^(Rss|Locked|Swap):' "/proc/$pid/smaps_rollup"
systemctl show llm-inference.service -p LimitMEMLOCK -p LimitMEMLOCKSoft -p MemoryCurrent -p MemoryMax
VmLck отвечает на узкий вопрос: сколько памяти процесс уже закрепил через механизмы семейства mlock. Он не равен RSS, CPU offload budget или размеру модели. Если приложение создаёт несколько worker-процессов, соберите показатель по каждому PID; один master с нулём не исключает десятки гигабайт в дочернем процессе.
Для infrastructure gate нужен ещё контролируемый отрицательный тест. Приложение должно успешно закреплять буфер внутри разрешённого бюджета и предсказуемо получать ошибку при попытке выйти за soft limit. Такой тест обнаруживает опасный случай, когда сервис получил CAP_IPC_LOCK и фактически обходит конечный rlimit. Проверяйте не только «можно», но и «граница действительно работает».
Не делайте вывод по одному cold start: page cache, lazy imports и JIT-компиляция смешиваются с transfer time. Сохраните baseline-конфигурацию, несколько повторов прикладного запроса, kernel/driver version и digest контейнерного образа. Эта дисциплина отличает измерение от удачного совпадения.
import ctypes
import errno
import mmap
import resource
import sys
size = int(sys.argv[1])
soft, hard = resource.getrlimit(resource.RLIMIT_MEMLOCK)
print({"requested": size, "soft": soft, "hard": hard})
buf = mmap.mmap(-1, size)
address = ctypes.addressof(ctypes.c_char.from_buffer(buf))
libc = ctypes.CDLL(None, use_errno=True)
rc = libc.mlock(ctypes.c_void_p(address), ctypes.c_size_t(size))
if rc != 0:
code = ctypes.get_errno()
raise OSError(code, errno.errorcode.get(code, "mlock failed"))
print("mlock: PASS")
libc.munlock(ctypes.c_void_p(address), ctypes.c_size_t(size))

Docker и Compose: конечный ulimit вместо лишней capability
В Docker параметр памяти контейнера и memlock решают разные задачи. --memory ограничивает общий объём memory cgroup, а --ulimit memlock задаёт process rlimit. Можно иметь большой --memory и всё равно падать на небольшой pinned allocation. Обратная комбинация ещё опаснее: высокий memlock при тесном cgroup-бюджете способен ускорить приближение OOM.
Официальная документация docker container run перечисляет memlock как максимальное locked-in-memory address space. Compose поддерживает soft/hard mapping через services.ulimits. Задавайте конкретное число, полученное из peak VmLck на canary, затем добавляйте небольшой эксплуатационный запас. Не копируйте 8 GiB из примера, если ваш offload буфер равен 40 GiB, и не ставьте unlimited на узле с несколькими арендаторами.
services:
inference:
image: registry.example/vllm@sha256:PINNED_DIGEST
gpus: all
ulimits:
memlock:
soft: 8589934592
hard: 8589934592
mem_limit: 48g
security_opt:
- no-new-privileges:true
command:
- vllm
- serve
- /models/current
После docker compose up -d выполните baseline-команды через docker exec. Если внутри по-прежнему виден старый лимит, контейнер мог не быть пересоздан, а только перезапущен. Зафиксируйте docker inspect, digest образа и effective /proc/1/limits. Configuration-as-code без effective-state проверки оставляет неприятную серую зону.
Capability IPC_LOCK даёт право, связанное с memory locking. Но mlock(2) отмечает, что привилегированный процесс с CAP_IPC_LOCK не ограничивается RLIMIT_MEMLOCK так же, как обычный. Поэтому сначала используйте конечный ulimit без capability. Добавляйте capability лишь тогда, когда конкретная библиотека и модель угроз действительно требуют её, и отдельно докажите, что вы не потеряли верхнюю границу.

systemd: закрепить политику в unit и проверить effective state
Для сервиса без контейнера rlimit лучше держать рядом с остальными production-ограничениями. Drop-in переживёт обновление package unit и будет виден в ревью. LimitMEMLOCK= задаёт soft и hard limit для процесса сервиса; если нужны разные значения, systemd допускает пару soft:hard. После изменения обязательны daemon-reload и restart: уже запущенный процесс не наследует новый лимит автоматически.
Размер считайте от фактического пика. Допустим, worker стабильно держит 5,6 GiB locked pages на целевой concurrency, а при reload кратковременно поднимается выше. Вы выбираете finite budget, который покрывает измеренный reload, но не позволяет одному процессу закрепить всю RAM. Это не универсальная формула: на другом backend или после обновления allocator профиль изменится.
[Service]
LimitMEMLOCK=8G
MemoryHigh=44G
MemoryMax=48G
NoNewPrivileges=yes
Затем проверьте сразу три слоя: systemctl show, /proc/$PID/limits и VmLck. Первый подтверждает декларацию manager, второй — наследованный process limit, третий — фактическое потребление. Зелёным infrastructure gate считается только их согласованность плюс успешный probe внутри бюджета.
MemoryHigh и MemoryMax здесь не заменяют memlock. Они задают общую политику cgroup и помогают увидеть конфликт между pinned buffer, RSS, page cache и sidecar-процессами. Именно такой конфликт часто выглядит как «GPU отваливается под нагрузкой», хотя устройство исправно. Для соседних I/O-нагрузок полезно отдельно пройти runbook по cgroup v2 и systemd на LLM-узле.
Rollback должен быть скучным: вернуть прежний drop-in, выполнить reload/restart и повторить baseline. Не оставляйте временный LimitMEMLOCK=infinity после расследования — временные параметры имеют привычку становиться архитектурой.

vLLM offload: где pinned memory становится частью data path
Для LLM inference pinned memory особенно заметна в сценариях, где данные продолжают ходить между host RAM и GPU после загрузки модели. Это weight offload и CPU KV-cache offload. В актуальной документации vLLM код offloader проверяет доступность pinning и использует отдельный override VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY; на unified-memory системах вроде GH200 pinning может расходовать общий memory budget иначе, поэтому там его можно отключить. Это хороший пример того, почему один рецепт не переносится на любую архитектуру.
CPU KV offload в текущей ветке vLLM регистрирует mmap-регион через cudaHostRegister, а при недоступном pinning предупреждает о возможном ухудшении производительности. Эти детали видны в официальном API reference для CPU offload worker. Однако offload-код быстро развивается. Закрепляйте версию vLLM и container digest, сверяйте flags через vllm serve --help именно в вашем образе и маркируйте экспериментальные пути в change record.
Application gate строится вокруг вашего рабочего сценария, а не synthetic copy alone. Для weight offload измеряйте cold start, TTFT первого запроса и steady-state p95. Для KV offload добавьте длинный контекст и concurrency, при которых CPU cache действительно используется. Параллельно снимайте VmLck, PCIe traffic, CPU utilization и memory pressure. Если pinned allocation проходит, но TTFT не улучшается, infrastructure gate уже не главный подозреваемый.
Сравнение должно включать отрицательный контроль: тот же image и model revision, но offload отключён либо pinning выключен документированным override. Возврат прикладной метрики к baseline подтверждает причинность. Если метрика не возвращается, вы, вероятно, измерили page cache, JIT или прогрев tokenizer, а не pinned data path.

Application gate: проверить PyTorch transfer и сам inference
Низкоуровневый mlock probe доказывает политику Linux, но не интеграцию CUDA allocator. Второй тест должен использовать тот же framework, что и сервис. В PyTorch можно выделить pinned tensor и выполнить non-blocking copy. Официальный PyTorch tutorial предупреждает о важной ловушке: ручной вызов pin_memory() в основном Python-потоке сам блокирует host и иногда делает путь медленнее. Значит, цель smoke test — корректность allocation и async API, а не обещание ускорения.
import os
import resource
import torch
size_mib = int(os.environ.get("PIN_TEST_MIB", "256"))
soft, hard = resource.getrlimit(resource.RLIMIT_MEMLOCK)
print({"memlock_soft": soft, "memlock_hard": hard, "test_mib": size_mib})
host = torch.empty(size_mib * 1024 * 1024, dtype=torch.uint8, pin_memory=True)
assert host.is_pinned()
device = host.to("cuda", non_blocking=True)
torch.cuda.synchronize()
print({"pinned": host.is_pinned(), "device": str(device.device), "status": "PASS"})
Запустите probe внутри production-контейнера до старта сервера или в отдельном canary с тем же runtime. Затем увеличьте PIN_TEST_MIB немного выше finite soft limit и убедитесь, что отрицательный тест действительно падает. Не запускайте заведомо огромный allocation на общем узле: границу можно проверить контролируемо, не создавая memory incident.
Третий уровень — сам inference. Сохраните одинаковые model files и tokenizer, сбросьте только те кэши, которые входят в сценарий, и повторите серию запросов. Не публикуйте универсальные проценты ускорения: результат зависит от PCIe topology, NUMA-locality, формата weights и доли transfers в критическом пути. Полезнее записать decision: при каком offload budget p95 улучшился, сколько VmLck это стоило и какой запас остался до MemoryHigh.
Если холодный старт определяется доставкой весов с registry, memlock начнёт влиять слишком поздно. Тогда сначала оптимизируйте model distribution и локальный NVMe-кэш; практический контекст есть в статье про OCI-артефакты и кэш весов LLM на NVMe. Это пример миграции bottleneck: после улучшения storage pinned transfer может стать заметен, но до него — нет.
Наблюдаемость: связать VmLck с latency и memory pressure
Один график locked memory мало что объясняет. Соберите три ряда. На process layer нужны VmLck, RSS, swap и restart count. На cgroup/host layer — memory.current, memory.events, PSI memory и доступная RAM. На application layer — cold-start duration, TTFT, p50/p95, очередь и ошибки allocation. Только совпадение временных рядов показывает, помогает ли pinning или просто забирает reclaimable memory.
Практический алерт лучше строить не на абсолютном «VmLck больше X», а на отношении к утверждённому budget и соседних симптомах. Например: locked pages приблизились к finite limit, в логах появились allocation failures, а TTFT вырос. Отдельный сигнал нужен для противоположной ситуации: VmLck стабилен, но memory PSI и memory.events растут. Тогда увеличение memlock ухудшит ситуацию.
В multi-worker архитектуре экспортируйте PID labels осторожно: они меняются после restart. Стабильнее собирать показатель по cgroup сервиса или агрегировать процессы по unit/container identity. При canary rollout сохраните конфигурационный fingerprint: image digest, CUDA driver, vLLM version, memlock soft/hard и offload flags. Без него сравнение через неделю превращается в археологию.
Не смешивайте locked host memory с GPU framebuffer metrics. DCGM и nvidia-smi покажут VRAM и состояние GPU, но не заменят /proc/$PID/status. Аналогично, высокий GPU utilization не доказывает, что data path оптимален: очередь может держать ускоритель занятым при плохом TTFT.
Rollout: два gate, canary и контрфактическая проверка
Начинайте с одного worker на одном GPU-узле. Зафиксируйте baseline при текущем memlock, затем установите finite limit, достаточный для измеренного буфера. Infrastructure gate: effective soft/hard совпадают с декларацией, positive probe проходит, negative probe выше границы падает, VmLck не превышает бюджет. Application gate: на той же модели и нагрузке улучшилась заранее выбранная метрика без роста ошибок и memory pressure.
После этого нужен counterfactual. Верните прежний memlock или отключите offload/pinning на том же canary и проверьте возврат метрики к baseline. Такой шаг кажется лишним, пока не выясняется, что одновременно прогрелся page cache или завершилась JIT-компиляция. Контрфактическая проверка защищает от ложной причинности.
Расширяйте rollout постепенно: один worker, затем один узел, затем небольшая доля пула. На каждом шаге проверяйте, не переехал ли bottleneck. Более быстрый CPU→GPU transfer может поднять нагрузку на NVMe, CPU allocator или frontend queue. Локально улучшив один счётчик, легко ухудшить end-to-end p95.
Rollback — отключение нового offload режима или возврат предыдущего finite limit и image digest. Он не должен требовать выдачи новых capabilities или ручной правки внутри контейнера. Для rootless GPU-контейнеров держите device access отдельно от memory policy; материал про NVIDIA CDI и rootless inference показывает, как не смешивать эти границы.

Типичные ошибки: unlimited, один benchmark и неверный слой
Первая ошибка — поставить memlock=-1 или LimitMEMLOCK=infinity, увидеть исчезновение ошибки и объявить проблему решённой. На dedicated single-tenant узле это может пройти незаметно, но в общем пуле один worker получает возможность закрепить слишком много RAM. Диагностический эксперимент допустим только на изолированном canary и должен закончиться конечным бюджетом.
Вторая ошибка — одновременно добавить IPC_LOCK, поднять ulimit, увеличить --memory и обновить vLLM. Если сервис ожил, вы не знаете почему. Меняйте один слой за раз, сохраняйте effective state и повторяйте отрицательный тест. Capability не является синонимом производительности; это изменение privilege boundary.
Третья ошибка — считать размер модели memlock-бюджетом. Весовые shards могут находиться в VRAM, pageable RAM, mmap, page cache или pinned region; KV cache живёт по другой схеме. Истина — peak VmLck конкретной версии приложения под целевым workload. Добавляйте запас на reload и allocator fragmentation только после наблюдения, не по магическому проценту.
Четвёртая ошибка — benchmark только на synthetic tensor. Он полезен для smoke test DMA-пути, но не включает tokenizer, scheduler, batching, storage и queueing. Финальное решение принимается по application gate. Проверяли ли вы длинный контекст, параллельные запросы и cold restart, или только один тёплый prompt?
Пятая ошибка — забыть об архитектуре памяти. На unified-memory платформе pinning может иметь другой trade-off; vLLM прямо предусматривает возможность его отключения для weight offload. На multi-socket сервере remote NUMA allocation способна съесть выгоду от pinning. Поэтому version lock, topology check и canary важнее универсальной строки конфигурации.
Итог: memlock — это бюджет, а не переключатель скорости
Pinned memory помогает там, где LLM inference действительно переносит заметные объёмы между host RAM и GPU: при weight offload, CPU KV-cache offload и асинхронных staging-пайплайнах. Но полезный эффект появляется не от большого числа в ulimit, а от совпадения четырёх условий: приложение использует page-locked buffers, процесс имеет достаточный конечный лимит, узел сохраняет запас обычной RAM, а прикладная метрика реагирует на изменение.
Надёжный runbook коротко выглядит так: снимите baseline из реального launcher, докажите границу через positive/negative probe, задайте finite memlock в Docker или systemd, проверьте VmLck вместе с cgroup pressure и выполните canary application test. Затем верните настройку назад и убедитесь, что метрика возвращается к baseline. Это превращает «кажется, стало быстрее» в проверяемое инфраструктурное решение.
Начните с одного worker и одной модели. Запишите image digest, версии CUDA/vLLM, offload flags, peak locked memory и выбранную метрику. Если выяснится, что bottleneck лежит в NVMe, NUMA или очереди, это тоже хороший результат: вы остановили бесконечное увеличение привилегий и получили следующий конкретный эксперимент.
Для production LLM важна не максимальная свобода процесса, а управляемая граница. Настройте её на canary, прогоните checklist и только после этого расширяйте rollout на GPU-пул или выделенный сервер.