Графические процессоры необходимы для ускорения рабочих нагрузок машинного обучения, включая обучение моделей с высокой пропускной способностью, вывод с учетом задержки и интерактивную разработку, обычно выполняемую в ноутбуках Jupyter (https://jupyter.org/). Распространенной практикой является развертывание заданий машинного обучения в виде контейнеров, управляемых Kubernetes (https://kubernetes.io/).

Kubernetes планирует рабочие нагрузки графического процессора, назначая все устройство исключительно одному заданию. Такая взаимосвязь приводит к массовому недоиспользованию графического процессора, особенно для интерактивных заданий, характеризующихся длительными периодами простоя и редкими вспышками интенсивного использования графического процессора. Текущие решения обеспечивают совместное использование графического процессора путем статического назначения фиксированного сегмента памяти графического процессора каждому совместно расположенному заданию. Эти решения не подходят для интерактивных сценариев, поскольку количество совместно размещаемых заданий ограничено размером физической памяти графического процессора. Следовательно, пользователи должны знать потребность в памяти GPU для своих заданий, прежде чем отправлять их на выполнение, что нецелесообразно.

1. Графические процессоры и Kubernetes

NVIDIA на сегодняшний день является самым распространенным поставщиком графических процессоров, когда речь идет о вычислениях на графических процессорах общего назначения. Google Cloud Platform (https://cloud.google.com/compute/docs/gpus) и Amazon Web Services (https://aws.amazon.com/ec2/instance-types/) почти исключительно предлагают NVIDIA графические процессоры.

Де-факто способ управления графическими процессорами в кластере Kubernetes — через подключаемый модуль устройства NVIDIA [1].

Для узла Kubernetes с N физическими графическими процессорами подключаемый модуль устройства объявляет кластеру N устройств `nvidia.com/gpu`.

Хотя контейнеры могут запрашивать дробные объемы ЦП и памяти, они могут запрашивать только целые числа для графических процессоров.

Таким образом, Kubernetes применяет назначение 1–1 между графическими процессорами и контейнерами. Даже если контейнер выполняет минимальную работу на графическом процессоре в течение всего своего жизненного цикла, этот графический процессор недоступен для остальной части кластера, пока контейнер работает.

[1]: https://github.com/NVIDIA/k8s-device-plugin/

2. Шаблоны использования графического процессора (сосредоточьтесь на случае машинного обучения)

Давайте рассмотрим машинное обучение как вариант использования ускорения графического процессора.

Операции линейной алгебры являются основой вычислений ML. Графические процессоры по своей природе являются массивно-параллельными и могут одновременно выполнять многие из этих простых операций линейной алгебры. Машинное обучение должно обрабатывать огромные объемы данных. Таким образом, высокая пропускная способность памяти графических процессоров используется для ускорения процесса разработки машинного обучения.

Задания по машинному обучению в K8s обычно попадают в эти три группы:

  1. Обучение
  2. Вывод
  3. интерактивная разработка/экспериментация

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

Практики машинного обучения обычно разрабатывают на Jupyter Notebooks. Блокноты Jupyter — это длительные интерактивные процессы, которые обеспечивают прекрасную среду для экспериментов, включающих повторение кода, внесение изменений и повторную калибровку модели до тех пор, пока результат не будет адекватным.

Задачи разработки машинного обучения (ноутбуки)

  • не выполнять заранее установленный объем работы; их время выполнения не может быть рассчитано/связано.
  • — это длительные задачи с моделями использования графического процессора, которые характеризуются всплесками и обычно имеют длительные периоды простоя (во время рефакторинга кода/отладки/перерывов разработчиков)

3. Проблема (почему связывание 1-1 — плохая идея)

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

Вот несколько случаев, когда пользователи озвучивают проблему:

  • «Графические процессоры не могут использоваться совместно — графические процессоры должны использоваться совместно» с форумов Jupyter [2]
  • «Возможно ли совместное использование GPU для нескольких контейнеров?» Ошибка Github в репозитории Kubernetes [3]

[2]: https://discourse.jupyter.org/t/gpus-can-not-be-shared-but-gpus-must-be-shared/1348/7
[3]: https://github.com/kubernetes/kubernetes/issues/52757

4. Почему NVIDIA выбрала привязку 1–1? (или корень проблемы)

Давайте представим сценарий, в котором эта привязка 1–1 не выполняется.

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

Поскольку процессы могут динамически увеличивать и уменьшать использование памяти графического процессора, а запросы на выделение обрабатываются в порядке FIFO (первым пришел — первым обслужен), возможны сценарии, в которых процессы завершаются с ошибкой нехватки памяти (OOM).

Хотя один процесс не может вмешиваться в содержимое памяти графического процессора другого, поскольку каждый процесс владеет отдельной таблицей страниц, NVIDIA не предоставляет возможности ограничить объем памяти графического процессора, выделяемый каждым процессом.

Эта надвигающаяся опасность серьезной ошибки OOM для любого совмещенного процесса GPU является причиной того, что Nvidia выбрала привязку 1–1 GPU-контейнера для своего подключаемого модуля устройства Kubernetes.

5. Настоящая проблема виртуализации GPU на K8s

Хотя проблема эксклюзивного назначения графических процессоров может быть решена тривиально (например, путем модификации подключаемого модуля вышестоящего устройства для объявления большего количества `nvidia.com/gpu`, чем физических графических процессоров), основная проблема заключается в управлении трением между совместно расположенными задачи, т. е. как ведут себя 2 или более процессов на одном узле, независимо от Kubernetes, см. пункт (4), и это сложно решить.

Следовательно, любое полное решение должно включать:

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

Также следует уточнить, что в любой момент только один контейнер (контекст в терминах CUDA) может активно использовать вычислительные блоки GPU. Драйвер графического процессора и аппаратное обеспечение обрабатывают переключение контекста нераскрытым образом. Этот промежуток времени, в течение которого контейнер выполняет исключительно вычисления на графическом процессоре, составляет порядка нескольких миллисекунд.

6. Существующие подходы к виртуализации и совместному использованию графических процессоров

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

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

Поскольку мы говорим о Kubernetes, все существующие подходы к совместному использованию графических процессоров, о которых мы упомянем, интегрированы с ним, т. е. они предлагают пользователям способ использовать совместно используемое устройство с графическим процессором так же, как и с `nvidia.com. /gpu` (по умолчанию).

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

A. Решения, игнорирующие проблему с памятью

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

Они предупреждают пользователей, что они должны «принять соответствующие меры», чтобы сумма использования памяти графического процессора не превышала физическую память графического процессора, и оставляют на их усмотрение, чтобы избежать ошибок OOM.

Такие известные подходы:

B. Решения, которые в некоторой степени решают проблему с памятью

Мы можем дополнительно классифицировать их на:

Б1. Те, которые реализуют нарезку памяти графического процессора без принудительного механизма:

Расширитель Aliyun GPU Sharing Scheduler
(https://github.com/AliyunContainerService/gpushare-scheduler-extender)
В этом подходе используется подключаемый модуль пользовательского устройства, который рекламирует ресурс aliyun.com/gpu-mem в кластер. Вместо того, чтобы запрашивать целочисленное количество графических процессоров, контейнеры теперь запрашивают мегабайты памяти графического процессора, а контейнеры Kubernetes упаковываются в контейнеры на основе их запросов памяти графического процессора.

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

Ошибки OOM по-прежнему могут возникать для любого контейнера, использующего GPU.

БИ 2. Те, которые обеспечивают разделение памяти графического процессора во время выполнения

Эти подходы аналогичны подходам в B1 в отношении планирования Kubernetes, т. е. они используют память графического процессора для упаковки контейнеров.

Kubeshare
(https://github.com/NTHU-LSALAB/KubeShare)
Этот подход не только использует память графического процессора для планирования запросов к контейнерам, но и обеспечивает выполнение этих запросы в действии. Например, контейнер, запросивший 500 МБ, может выделить не более этого объема памяти графического процессора во время выполнения. Если он попытается выделить больше, произойдет сбой с ошибкой OOM.
Kubeshare достигает этого, перехватывая вызовы выделения памяти CUDA API и гарантируя, что контейнер не сможет выделить больше памяти, чем он запросил у Kubernetes.

TKEStack GPU Manager
(https://github.com/tkestack/gpu-manager)
Этот подход очень похож на Kubeshare. Он планирует контейнеры на основе их запросов памяти графического процессора, применяя их во время выполнения.

7. Недостатки существующих подходов

Жесткое ограничение памяти
Все существующие подходы налагают жесткое ограничение на память графического процессора, которое пользователи должны указать при отправке своей рабочей нагрузки. Обычно невозможно заранее узнать пиковое использование памяти заданием, особенно когда речь идет об интерактивном задании разработки. Это жесткое ограничение ограничивает гибкость рабочего процесса пользователя в отношении тестирования постепенно увеличивающихся моделей. Им придется либо запрашивать большой объем памяти, которая в основном останется неиспользованной, либо потенциально столкнуться с ошибкой OOM.

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

8. Оценка текущего состояния дел

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

Эти существующие подходы к совместному использованию графического процессора подходят для вывода ML, поскольку размер памяти графического процессора предсказуем.

Однако этот новый критерий в сочетании с характером рабочих нагрузок с нечастыми всплесками производительности графического процессора (такими как исследование интерактивной модели машинного обучения на ноутбуках Jupyter) по-прежнему допускает сценарии недостаточного использования.

Давайте представим сценарий, в котором GPU имеет 4 ГБ памяти, и пользователь сначала отправляет задание A с запросом на 2,5 ГБ памяти, а другой пользователь отправляет задание B с запросом на 2 ГБ.

Задание B никогда не будет запланировано, пока выполняется задание A, независимо от фактического использования памяти заданием A. Это допустимо в случае задач с интенсивными вычислениями (обучение машинному обучению), однако для интерактивных задач графический процессор остается недостаточно загруженным на протяжении всей неопределенной продолжительности задания А.

Arrikto стремится продвигать новейшие достижения в области виртуализации графических процессоров в Kubernetes и помогать специалистам по машинному обучению и предприятиям в оптимизации их вычислительных и облачных затрат. Kiwi (https://docs.arrikto.com/release-2.0/features/kiwi.html), функция Arrikto Sharing GPU, доступна в качестве технической предварительной версии в EKF 2.0. Мы обсудим, как мы разработали Kiwi для устранения вышеупомянутых ограничений, в следующем посте в блоге — следите за обновлениями!

Приложение

ML Frameworks обрабатывает память графического процессора

Фреймворки машинного обучения предпочитают обрабатывать память графического процессора через внутренние вспомогательные распределители. Таким образом, они запрашивают память графического процессора большими порциями и обычно превышают реальный требование. Кроме того, Tensorflow [4] по умолчанию выделяет всю память графического процессора. Это поведение можно при необходимости изменить, чтобы увеличить использование памяти графическим процессором по мере необходимости. Тем не менее, это использование никогда не будет сокращаться. Если фактическая потребность задания ML колеблется от 500 МиБ до 2,5 ГиБ, а затем обратно до 500 МиБ, то выделенная память графического процессора будет составлять 2,5 ГиБ до завершения процесса TF.
[4]: ​​https://www. tensorflow.org/guide/gpu#limiting_gpu_memory_growth

Первоначально опубликовано на https://www.arrikto.com 2 ноября 2022 г.