Расширители памяти на CXL сделали то, чего от них ждали: память за пределами процессора стала почти обычной памятью, с обычными указателями, обычными страницами и поддержкой со стороны ядра. Но между установленным устройством и полезной полосой стоит настройка чередования, и от неё зависит, получит ли приложение широкую полосу из нескольких устройств или узкую, зато предсказуемую, дорожку из одного. Механизм называется interleaving, он программируется декодерами на всех уровнях от корневого порта до конечного устройства, и Linux начиная с серии 6.x умеет видеть, конфигурировать и контролировать всё это через подсистему cxl и утилиты из набора ndctl.

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

Как память расширителя попадает в адресное пространство хоста

Устройство класса Type-3, классический расширитель, говорит с хостом протоколом CXL.mem поверх физики PCIe. Память устройства называется управляемой хостом, Host-managed Device Memory, и она попадает в физическое адресное пространство через декодеры: прошивка объявляет фиксированные окна памяти в таблице CEDT, ядро создаёт соответствующие декодеры корневого порта, а затем программирует декодеры на пути и на самом устройстве. С точки зрения приложения получается устройство DAX, доступное как devdax через отображение в память или как fsdax через файловую систему, и никаких блочных уровней с их копированиями на пути нет. Запись CFMWS в таблице CEDT несёт всё нужное для старта: базу и размер окна, число путей и шаг чередования по умолчанию, идентификатор группы качества сервиса, и именно из этих записей ядро собирает первичную карту доступной памяти.

В системе это выглядит так:

ls /sys/bus/cxl/devices/
cxl list -B -D -M -R

Память появляется как отдельный NUMA-узел без процессоров, и планировщик ядра умеет работать с таким узлом по общим правилам NUMA. Прошивка через таблицы ACPI описывает не только окна, но и качество сервиса: записи HMAT и данные CDAT, которые устройство отдаёт через почтовый ящик DOE, рассказывают о задержках и полосе, а группы QTG подсказывают, какие трафики с какими окнами совместимы. Базовая поддержка подсистемы вошла в ядро 5.17, работа с регионами устоялась к выпуску 6.0, а утилита cxl живёт в комплекте ndctl начиная с версии 71.

Уровни чередования и параметры декодеров

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

Выбор шага это выбор профиля, и наглядно это видно на арифметике страниц. При шаге в 256 байт обычная страница на четыре килобайта разворачивается в шестнадцать кусков на шестнадцати разных устройствах, и линейный поток заполняет все пути равномерно, выжимая максимум полосы. При шаге в четыре килобайта та же страница целиком лежит на одном устройстве, локальность и предсказуемость одиночных обращений сохраняются, зато короткий поток заполняет лишь часть путей. Мелкий шаг размазывает последовательный поток по всем путям и выжимает максимум полосы для больших линейных копирований, крупный держит страницы целыми и дружит со страничным поведением приложений. Крупный шаг держит страницы целыми на одном устройстве, сохраняет предсказуемую задержку и дружит со страничным поведением приложений, зато короткие последовательные потоки не умеют заполнять все пути одновременно. Разумное правило: стриминговым рабочим нагрузкам и копированию больших объёмов мелкий шаг, латентно-чувствительным службам и смешанным нагрузкам крупный, а финальное слово всегда за замером.

Сборка региона из устройств

Регионы собираются утилитой cxl поверх видимых декодеров:

cxl create-region -d decoder0.0 -m mem0 -m mem1 -w 2 -g 4096
ndctl create-namespace --mode=fsdax --region=region0
ndctl list -R

Первая команда берёт декодер корневого порта, чередует два устройства памяти с шагом четыре килобайта и создаёт регион, вторая заводит на нём пространство имён в режиме fsdax, пригодное для файловой системы, третья показывает результат. В режиме devdax приложение отображает устройство в память напрямую, в fsdax поверх лежит обычная файловая система, и оба пути дают одинаковый доступ к чередованной памяти. Несовпадение числа путей с числом доступных устройств ядро откажется принимать, так же как и несогласованные шаги между уровнями, и сообщения об ошибках в таких случаях достаточно внятны.

Обратная операция симметрична: регион уничтожается командой cxl destroy-region с ключом принудительного удаления, декодеры освобождаются, и их можно переконфигурировать под другую схему без перезагрузки. Регионы умеют собираться и за переключателями: декодеры переключателя программируются ядром в ходе сборки, и с точки зрения хоста это такой же регион с большим числом путей. Для ферм, где раскладка памяти меняется вместе с профилем нагрузок, это делает чередование обычной операцией провижининга, а не одноразовой настройкой при сборке узла.

Чередование с DDR и NUMA-топология

Отдельный вопрос, чередовать ли память CXL с обычной DDR. Часть платформ умеет платформенное чередование между контроллером DDR и окнами CXL, тогда память сливается в один узел и выглядит однородной, но платформа обязана честно объявить такую схему в прошивке, иначе однородность окажется ложной. Более распространённый и прозрачный путь, отдельный NUMA-узел для CXL-памяти, и здесь в игру вступает уровневость памяти, появившаяся в ядре 6.1: узлы сортируются по уровням, CXL попадает на уровень ниже DDR, и ядро использует его как приёмник для демоушна холодных страниц, высвобождая быструю память под горячие данные.

Расстояния между узлами платформа объявляет в таблицах SRAT и HMAT, и от их честности зависит, насколько разумно планировщик будет раскладывать задачи и память. Управление со стороны администратора стандартное: привязки через numactl по узлам, контроль раскладки через /sys/devices/system/node и уровни в /sys/devices/virtual/memory_tiering. Для приложений с явным управлением памятью отдельный узел даже удобнее, потому что поместить рабочий набор или буфер обмена именно на CXL можно одной привязкой. Механика демоушна обычная для NUMA-балансировки: страницы, к которым давно не обращались, мигрируют на узел уровнем ниже, освобождая быструю память, а при повторных обращениях страница переезжает обратно, если процессор рядом с ней активен. Полоса и число таких миграций видны в счётчиках pgmigrate в /proc/vmstat, и на узлах с активным демоушном в CXL-память эти числа становятся одной из основных метрик. Для приложений безразличных, просто выделяющих память, работает именно эта схема, и чередование здесь определяет, насколько широкой будет полоса на приёмник.

Полоса и задержка чередованной памяти на практике

Физика накладывает рамку. CXL.mem идёт поверх линий PCIe, и линия PCIe 5.0 с шестнадцатью линиами даёт порядка 64 гигабайт в секунду в каждую сторону, линия четвёртого поколения вдвое меньше. Задержка доступа к памяти за CXL выше локальной DDR на величину порядка сотни наносекунд, переключатель в пути добавляет ещё немного, и это цена, которую платят за гибкость. Чередование полосу умножает: два устройства с шагом 256 байт дают близкую к удвоенной полосу на линейном потоке, восемь устройств приближаются к потолку линий.

Задержка чередованием почти не лечится, и это важно понимать при проектировании. Если приложению нужен быстрый ответ на одиночное обращение, CXL память даже с крупным шагом проигрывает локальной DDR, и её место в системе холодных данных, реплик, кэшей второго уровня и объёмных рабочих наборов, где важна суммарная полоса. Проверять ожидания стоит банально: fio по устройству с разными глубинами очереди, замер на одном устройстве и на чередованной паре, и разница покажет, что реально даёт выбранная схема. Для чистой задержки берут очередь глубиной один и мелкий блок, для полосы крупные блоки и глубокие очереди, а привязка процесса к узлу рядом с окном памяти через numactl убирает лишнюю переменную из сравнения. На агрегатах из нескольких устройств упор приходит быстро: суммарная полоса четырёх расширителей уже упирается не в сами устройства, а в общий путь от корневого порта, и замер это показывает без всяких иллюзий.

Порядок выбора конфигурации чередования на узле

Выбор рабочей схемы сводится к последовательности:

  1. снять карту устройств и декодеров через cxl list и убедиться, что прошивка выставила окна согласованно;
  2. классифицировать нагрузки узла на стриминговые и латентно-чувствительные, потому что им нужны противоположные шаги;
  3. выбрать число путей по числу устройств и шаг по профилю доступа, помня о перемножении уровней;
  4. собрать регион и пространство имён, прогнать fio на эталонной нагрузке и сравнить с замером одного устройства;
  5. при многопользовательской раздаче привязать классы к узлам через numactl, оставив демоушн для безымянной холодной памяти;
  6. закрепить схему в провижининге узла и в документации, потому что восстановить её по памяти через полгода не выйдет.

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

Подводные камни и диагностика

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

Диагностические источники при этом богатые. Журнал ядра пишет программирование декодеров, подсистема поддерживает трассировочные события cxl в /sys/kernel/tracing, и их набор говорящий: события носителя, события динамической памяти и события модуля памяти приходят с устройства и показывают не только ошибки, но и здоровье носителя в динамике. Чтение CDAT через cxl list с подробными ключами показывает заявленные устройством задержки и полосу, что удобно сверять с фактическими замерами и с обещаниями вендора. Систематический подход прост: сначала cxl list, затем журнал, затем трассировка под нагрузкой, и почти любой сюрприз чередования находит объяснение в этих трёх местах. Отдельная ловушка относится к скорости развития стека: утилиты и ядро меняли ключи и имена, и команды из старых инструкций на свежих версиях ndctl иногда требуют другой записи, поэтому примеры стоит проверять по справке установленной версии, а не копировать из заметок двухлетней давности.

В итоге чередование определяет, чем именно станет CXL-память на узле: широкой полосой для потоков, предсказуемой дорожкой для холодных данных или приёмником демоушна для чужих страниц. Ядро и утилиты дают полный контроль над этим выбором, а цена ошибки измеряется не переделкой железа, а десятком минут на пересборку региона с новыми параметрами, что делает эксперименты дешёвыми, а правильный выбор достижимым.