Регистрация памяти (Memory Registration) - фундаментальный механизм RDMA (Remote Direct Memory Access), отличающий его от традиционных сетевых стеков. Прежде чем NIC (сетевой адаптер) сможет читать или записывать удалённую память напрямую, минуя CPU, эта память должна быть зарегистрирована: драйвер фиксирует её физические страницы, получает ключи доступа (lkey/rkey) и закрепляет страницы в оперативной памяти. Понимание регистрации критично, поскольку именно она определяет накладные расходы на установку соединений, потребление ресурсов pinned-памяти и возможность использования огромных страниц для высокопроизводительных приложений.

Разберём жизненный цикл регистрации, работу ключей доступа, взаимодействие с подсистемой памяти ядра, оптимизации и типичные ошибки, с которыми сталкиваются разработчики при переходе от TCP к RDMA.

Зачем нужна регистрация памяти в RDMA

Обычная сеть передаёт данные через буферы ядра: приложение копирует данные в kernel space, сетевой стек упаковывает их в пакеты, NIC отправляет. RDMA устраняет копирование: NIC обращается к памяти приложения напрямую через DMA. Для этого нужно решить две задачи.

Первая - преобразование виртуальных адресов в физические. Приложение работает с виртуальными адресами своего процесса, а NIC оперирует физическими адресами через DMA. Драйвер при регистрации обходит таблицы страниц процесса, собирает массив физических страниц и загружает его в MTT (Memory Translation Table) адаптера. Вторая - защита. Зарегистрированная память получает ключи: локальный (lkey) для операций локального процесса и удалённый (rkey), который передаётся удалённой стороне и разрешает ей доступ к буферу. Ключи содержат индекс в таблице трансляции и биты разрешений (чтение, запись, atomic-операции).

Регистрация также закрепляет (pins) страницы в памяти: они не могут быть выгружены в swap или перемещены при компакции. Это обязательное требование DMA - NIC не может обработать страничное прерывание посреди операции. Закрепление имеет цену: pinned-память недоступна для других процессов, поэтому её объём ограничивается настройкой vm.max_map_count и лимитами memlock для пользователя (ulimit -l). На узлах с большим объёмом памяти, отданным под RDMA, необходимо планировать распределение между обычными процессами и закреплёнными буферами заранее, иначе рост файлового кэша и обычных приложений начнёт конфликтовать с закреплёнными областями при обновлениях и перезапусках сервисов.

Полный жизненный цикл регистрации буфера

Регистрация через verb ibv_reg_mr проходит несколько этапов. Приложение выделяет буфер (обычно выровненный по странице), вызывает verb с указанием длины и прав доступа. Драйвер фиксирует страницы через get_user_pages, строит список физических адресов, распределяет MTT в адаптере, создаёт MR-объект и возвращает структуру с lkey и rkey. После завершения работы вызывается ibv_dereg_mr, освобождающий таблицы и снимающий закрепление. Сложность операции пропорциональна числу страниц: регистрация гигабайтного буфера занимает миллисекунды, мегабайтного - микросекунды.

Три режима доступа к памяти определяют семантику:

  • Регистрация буфера: классический режим, каждый буфер регистрируется отдельно, ключи привязаны к конкретному диапазону адресов.
  • Memory Window: гибкий механизм, позволяющий удалённой стороне получать доступ к разным частям зарегистрированной памяти через разные ключи с разными правами; окно привязывается (bind) к MR в момент операции.
  • Implicit ODP (On-Demand Paging): страницы подгружаются по требованию через страничные ошибки, регистрируется всё адресное пространство сразу.

Права доступа задаются флагами IBV_ACCESS_LOCAL_WRITE, IBV_ACCESS_REMOTE_WRITE, IBV_ACCESS_REMOTE_READ, IBV_ACCESS_REMOTE_ATOMIC и IBV_ACCESS_RELAXED_ORDERING (для включения relaxed ordering в PCIe). Для remote write и atomic требуется дополнительно local write, поскольку адаптер должен иметь возможность модифицировать буфер для служебных целей.

Ключи и таблицы трансляции в оборудовании

MTT хранится в памяти адаптера или системной памяти, отображённой в адресное пространство NIC. Каждая запись описывает физическую страницу. Ключ MR кодирует индекс в таблице и номер поколения (generation counter), что предотвращает использование устаревших ключей после перерегистрации: попытка операции со старым rkey завершится ошибкой, а не чтением чужих данных.

Для больших областей памяти поддерживаются огромные страницы: одна запись MTT описывает 2 МБ или 1 ГБ вместо 4 КБ, что сокращает размер таблицы в сотни раз и снижает TLB-промахи в NIC. Регистрация через огромные страницы особенно эффективна для ML-нагрузок с тензорами в гигабайты, где иначе таблицы MTT занимали бы сотни мегабайт.

RDMA NIC также имеет кэш трансляции (translation cache), ускоряющий повторяющиеся обращения. При invalidate кэша ключа (после dereg или bind окна) происходит инвалидация кэша трансляции на всех портах, что является дорогой операцией и может останавливать DMA на микросекунды. Поэтому частые invalidate в горячем пути заметно снижают пропускную способность.

Число записей MTT - ограниченный ресурс адаптера: на старых HCA доступно несколько сотен тысяч записей, чего при 4 КБ страницах хватает лишь на пару гигабайт. Современные NIC поддерживают миллионы записей, но объём по-прежнему стоит учитывать при проектировании: если приложение регистрирует тысячи маленьких областей, таблицы могут исчерпаться, и регистрация начнёт падать с ошибкой, хотя системной памяти достаточно. Диагностировать ситуацию можно через счётчики выделенных MTT в sysfs адаптера.

Пул памяти и оптимизация регистрации

Регистрация - дорогая операция: обход таблиц страниц, выделение MTT, закрепление страниц. Для приложений с множеством короткоживущих буферов (например, RPC-фреймворков) прямой подход неприемлем. Решение - пул памяти (memory pool): большой буфер регистрируется один раз при старте, а приложение раздаёт из него фрагменты. SGL (Scatter-Gather List) позволяет описывать сообщение несколькими фрагментами из пула, каждый со своим lkey, что устраняет копирование при сборке сообщения из частей.

Второй подход - кэширование регистраций: если тот же буфер регистрируется повторно, возвращается прежний MR. Библиотеки вроде ucx, libfabric и MPI-реализации (OpenMPI, MPICH) строят такие кэши автоматически. Третий - ODP, при котором регистрация становится тривиальной (регистрируется всё адресное пространство), а цена переносится на страничные ошибки во время работы; подходит для кода без жёстких требований к латентности и упрощает управление памятью (malloc можно использовать как обычно).

Отдельная оптимизация - регистрация в режиме relaxed ordering: NIC может переупорядочивать DMA-чтения и записи, что улучшает производительность на современных PCIe-топологиях, но требует поддержки со стороны NIC и согласования в драйвере. Для периферии с большими объёмами данных (NVMe-oF, GPU) выигрыш достигает 5-10 процентов.

При выборе стратегии стоит опираться на профиль allocations приложения. Если буферы создаются один раз при старте и живут до завершения, прямая регистрация оптимальна и не требует дополнительной инфраструктуры. Если буферы циклически создаются и уничтожаются (например, по одному на запрос), пул окупается немедленно: накладные расходы на ведение пула на порядки ниже расходов на регистрацию. Если же схема памяти заранее неизвестна (плагинная архитектура, скриптовые обвязки), кэширование регистраций на уровне библиотеки обмена сообщениями - компромисс между гибкостью и производительностью.

Взаимодействие с подсистемой памяти ядра

Регистрация взаимодействует с несколькими механизмами ядра. get_user_pages (в современных ядрах - pin_user_pages) фиксирует страницы и инкрементирует счётчик refs, предотвращая миграцию и выгрузку. Обратная операция unpin_user_pages выполняется при dereg. Ядро отслеживает общий объём закреплённой памяти и при превышении лимитов отказывает в регистрации.

Переносимые (migratable) страницы, которые нельзя закрепить навсегда (например, в зоне movable), вызывают проблемы при прозрачных огромных страницах (THP): попытка регистрации THP-страницы может расщепить её на 4 КБ страницы, что снижает производительность и разрастает MTT. Для ML-нагрузок рекомендуются явные hugepages через hugetlbfs: зарегистрированные там страницы уже огромные и стабильные, расщепления не происходит.

Отдельный механизм - peer memory (NVIDIA GPUDirect): регистрация памяти GPU позволяет NIC обращаться напрямую в VRAM через PCIe P2P, минуя системную память. Драйверы peer_memory_client (в ядре) и cuMemGetAddressRange (в CUDA) позволяют зарегистрировать device-буфер, что сокращает латентность GPU-to-GPU обмена в кластерах на 20-40 процентов и снимает нагрузку с системной шины. Аналогичные механизмы поддерживаются для CXL-памяти и DPU.

Отладка регистрации в среде разработки упрощается верб-трассировкой: модуль ядра rdma_core умеет логировать каждую операцию регистрации с параметрами и временем выполнения (через tracepoints RDMA), что помогает найти неожиданно дорогие регистрации в горячем пути. В бенчмарках (perftest, ib_write_bw) параметры памяти фиксированы, поэтому их результаты не показывают цену регистрации - её следует измерять отдельно, профилируя инициализацию приложения и замеряя время между запуском и готовностью к первой операции обмена.

Практические приёмы и типичные ошибки

Первая ошибка - превышение лимита memlock: регистрация падает с ENOMEM, хотя память есть. Лимит настраивается в /etc/security/limits.conf (<user> hard memlock unlimited) или через systemd-юнит (LimitMEMLOCK=infinity). Вторая - регистрация больших буферов с малым количеством hugepages: таблицы MTT разрастаются, NIC начинает трэшить кэш трансляции. Третья - повторная регистрация в горячем пути: даже при кэшировании накладные расходы на поиск в кэше заметны при десятках тысяч операций в секунду, поэтому буферы следует выносить в пул на этапе инициализации.

Четвёртая ошибка - использование relaxed ordering без проверки поддержки: на старом оборудовании включение флага приводит к падению или тихой деградации. Пятая - утечка MR: забытое dereg накапливает pinned-память, что приводит к исчерпанию ресурсов и падению других приложений. Инструмент ibv_devinfo показывает статистику MR, а /sys/class/infiniband/<device>/ports/1/counters/ - счётчики ошибок трансляции, по которым можно диагностировать проблемы с ключами и таблицами.

Порядок настройки для ML-кластера:

  1. выделить hugepages (vm.nr_hugepages из расчёта 2 МБ страниц на объём буферов) и смонтировать hugetlbfs;
  2. поднять memlock для пользователя или сервиса (unlimited или явный объём);
  3. зарегистрировать пулы буферов один раз при инициализации приложения с нужными правами;
  4. для GPU-нагрузок настроить GPUDirect RDMA и зарегистрировать device-память через peer memory;
  5. проверить счётчики NIC (out_of_sequence, rnr_nak) на предмет ошибок трансляции;
  6. настроить мониторинг объёма pinned-памяти и числа MR, чтобы выявлять утечки до исчерпания ресурсов.

Заключение по регистрации памяти RDMA

Регистрация памяти - не вспомогательная деталь, а ключевой фактор производительности RDMA-приложений. Правильный пул буферов, огромные страницы, GPUDirect и корректные права доступа сокращают накладные расходы на порядок и убирают скрытые источники латентности. Обратная сторона - управление ресурсами: pinned-память, лимиты memlock и утечки MR требуют дисциплины в коде и мониторинга в эксплуатации. Приложения, которые относят регистрацию к этапу инициализации и тщательно контролируют ресурсы, получают полный выигрыш RDMA - латентность в микросекунды и пропускную способность, ограниченную только шиной PCIe.