Usando Nvidia GPU’s no Google Kubernetes Engine

Jonatan Michael
Único
Published in
7 min readJan 6, 2021

Recentemente me deparei com a necessidade de rodar alguns microsserviços que utilizam Nvidia GPU no Google Kubernetes Engine (GKE) e encontrar informação detalhada sobre o assunto não foi tão trivial.

Neste post vamos falar sobre o uso de Nvidia GPU no GKE, quais suas dependências e como tudo se relaciona, desde o driver (módulo kernel) necessário para utilizar o recurso físico, as bibliotecas (CUDA) e tudo que precisamos saber e ter (a nível de ambiente) para rodar containers com o mínimo de dependência possível (ps: se você não roda seus workloads no GKE não se preocupe, o que vamos discutir abaixo serve para kubernetes em geral).

Um pouquinho de história

Um dos princípios básicos de containers é que você pode empacotar uma aplicação e suas dependências e rodar em qualquer lugar, ser portável. Sendo assim, uma vez que você tem a imagem de um container, você não deveria se preocupar com dependências e conflitos com outras aplicações. Você deve poder rodar suas imagens de containers em qualquer lugar.

Pois bem, isso quase sempre é verdade, exceto quando uma das dependências da sua aplicação é um módulo kernel.

Containers por si só já dependem do kernel do sistema operacional (SO), e por isso você não precisa de um SO completo rodando dentro do seu container. Ele usa o kernel do sistema operacional, e o host onde sua aplicação rodará deve ter esse modulo kernel instalado. Até aqui tudo bem!

O problema surge quando seu container requer extensões e/ou módulos adicionais/customizados no kernel. E este é o nosso caso, já que usar Nvidia GPU requer o módulo Nvidia kernel (nvidia.ko) instalado no host. Além de também requerer a presença de bibliotecas CUDA (libnvidia-ml.so, libcuda.so, etc) dentro do container. E não acaba por aí, já que também dependemos do CUDA Toolkit como ferramenta de desenvolvimento.

Ou seja, para acessar a GPU, sua aplicação precisa de pelo menos estes 3 componentes abaixo:

  1. Nvidia GPU Driver: Kernel-mode driver component.
  2. CUDA Driver: User-mode driver component.
  3. CUDA Toolkit: User-mode SDK.
CUDA — Fonte: https://docs.nvidia.com/deploy/cuda-compatibility/index.html#cuda

É bastante coisa, não? Sim. É bastante coisa.

Mas afinal o que exatamente precisa estar dentro do meu container?

O container precisa ter 2 componentes: o CUDA Toolkit (que é uma ferramenta de desenvolvimento) e precisa ter acesso as bibliotecas do CUDA Driver (este é o componente que permite sua aplicação se comunicar com o Nvidia kernel). E é isso.

Então onde está o problema? Não é só garantir que o Nvidia Kernel esteja instalado no host e então criar uma imagem docker com as outras dependências?

A resposta é SIM e NÃO. Vai funcionar? Vai. Porém, existe uma forte dependência da versão CUDA driver com a versão do Nvidia Kernel, as versões precisam ser iguais. Sendo assim seu container fica limitado a rodar somente em hosts que contenham o Kernel em uma versão idêntica a versão do CUDA driver que está dentro da imagem do seu container. E pronto, você acaba de perder uma das principais vantagens de usar container. Ele não é mais portável.

Já imaginou ter que manter Kubernetes Nodes e controlar a versão exata do Nvidia Kernel que roda em cada Node para saber qual container/pod subir dependendo da versão do Kernel vs CUDA? Melhor nem imaginar. Na sequencia vamos ver como Kubernetes nos ajuda a resolver este problema.

Kubernetes Device Plugin

Kubernetes fornece um mecanismo para que fornecedores de dispositivos (como GPUs) possam criar suporte a seus dispositivos com código específico. Dessa forma, a Nvidia tem seu próprio Device Plugin o qual está personalizado para resolver algumas dependências quando temos Kubernetes Nodes com GPUs.

E como os Devices Plugins resolvem o nosso problema de dependência entre versões de Kernel (no host) e Cuda Driver (no container)?

Através de uma API que permite setar variáveis e até montar volumes dentro do container. Dessa forma, basta você informar no seu PodSpec (veremos um exemplo prático um pouco mais abaixo) que sua aplicação vai usar GPU e automaticamente será mapeado um volume com as bibliotecas do CUDA Driver (compatível com a versão instalada no host) dentro do seu container. A imagem do seu container não precisa ter o CUDA Driver. Não é lindo?

Quer saber mais sobre K8s Device Plugins? Clica aqui.

E o CUDA Toolkit? Não esquecemos dele?

Pois é. Você ainda precisa do CUDA Toolkit na imagem do seu container e ele tem algumas dependências da versão do kernel e CUDA driver. Mas essa dependência não é tão onerosa. O Toolkit não requer que você tenha a versão exata do kernel, mas sim uma versão mínima.

CUDA Toolkit and Compatible Driver Versions — Fonte: https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility

Outro ponto a favor é que novas versões do CUDA Driver tem retrocompatibilidade com as versões antigas do Toolkit. Assim, você pode atualizar o Nvidia Kernel e o CUDA Driver no host sem a obrigação de atualizar a imagem do seu container com um novo CUDA Toolkit.

Node Components — Fonte: https://docs.mellanox.com/pages/releaseview.action?pageId=15049828

Ufa! Que tal um pouco de prática?

Agora vamos entender como tudo isso funciona no GKE. Não é nosso intuito ensinar neste post como criar um Cluster K8s no Google Cloud Platform (GCP) e também não vamos ensinar a criar Node Pools com GPU. Se você não sabe como concluir esses passos, por favor consulte a documentação oficial antes de seguir.

Instalando dependências

Uma vez que seu cluster tenha Node Pools com GPU, o primeiro passo é instalar o Nvidia Kernel e o CUDA Driver em cada um desses nodes. Para isso, vamos utilizar o DaemonSet fornecido pelo próprio GCP. Este DaemonSet garante que o Pod chamado “nvidia-driver-installer” rode em todo Node com GPU. E o que este Pod faz? Ele instala e configura o Nvidia kernel, o CUDA Driver e inicia o Pod do Device Plugin (“nvidia-gpu-device-plugin”):

kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/container-engine-accelerators/master/nvidia-driver-installer/cos/daemonset-preloaded.yaml

Ps: a Nvidia tem seu próprio Device Plugin, porém, neste caso estamos instalando um fornecido pelo próprio GKE. Este tem algumas vantagens, como por exemplo não requerer a instalação do Nvidia Docker (personalização criada pela própria Nvidia), sendo compatível com qualquer Container Runtime Interface (CRI).

Requisitando GPU no PodSpec

Com a imagem do container pronta (incluindo o CUDA Toolkit, não se esqueça!), ao criar o PodSpec você precisa especificar que seu container utilizará GPU.

Algumas dicas sobre “limits” e “requests” de GPU(s):

  1. Você pode especificar apenas “limits” de GPU já que por padrão o kubernetes irá utilizar o mesmo valor dos “limits” para “requests”.
  2. Você até pode especificar ambos os valores “limits” e “requests”, mas os dois valores precisam ser iguais.
  3. Você não pode especificar “requests” sem especificar “limits”.
  4. É possível requisitar uma ou mais GPUs por Container.
  5. Não é possível requisitar uma fração de GPU (não tem como compartilhar a mesma GPU entre 1 ou mais Containers).

Ao especificar que seu container utilizará GPU, automaticamente o Kubernetes irá subir seu Pod em um node que contenha GPU disponível. Se seu cluster não tiver nodes com GPU ociosa, seu Pod não será iniciado.

Lembrando que ao requisitar GPU, também serão resolvidas de forma automática as dependências relacionadas as bibliotecas do CUDA Driver (lembra do DaemonSet que instala as dependências e inicia o device plugin?). As bibliotecas necessárias serão mapeadas através de um volume pra dentro do seu Container.

Taints e Tolerations

Para evitar que qualquer Pod (que não requer GPU) seja iniciado em nodes contendo GPUs, GKE cria taints. Uma vez que um node k8s possui um taint específico, nenhum Pod será rodado nele sem ter uma configuração de toleration específica para esse taint.

Os taints servem para limitar o que pode rodar em um determinado node. E os tolerations servem para informar ao kubernetes que ele pode rodar o Pod em nodes que contenham um determinado taint.

Quando seu container requer GPU, automaticamente os tolerations serão adicionados ao seu Pod para que ele possa subir nos nodes com GPU, que por sua vez contém os taints específicos.

O que mais devo saber?

  1. Cotas de GPU no GCP: os recursos de GPU são utilizados com cotas reservadas. As cotas são configuradas por região/zonas e por modelo de GPU.
  2. Não é possível adicionar GPUs a Node Pools existentes.
  3. Os nodes com GPU não são migrados em tempo real durante os eventos de manutenção. Eles devem ser configurados para “parar” durante eventos de manutenção e posteriormente “iniciar” de forma automática.
  4. GPUs são suportadas apenas em VM’s de propósito geral (tipo N1).
  5. As GPUs estão disponíveis em regiões e zonas específicas. Fique atento e faça um planejamento com antecedência caso queira rodar seus workloads em mais de uma região/zona.
  6. O Kubernetes não identifica GPU por tipo (ex: Tesla P100 e Tesla V100), portanto, aconselha-se criar labels nos seus nodes de forma que eles identifiquem o tipo de GPU que contém. E no seu PodSpec você pode utilizar a opção NodeSelector para selecionar nodes com GPUs específicas.

Parece muito trabalho, mas muitas coisas já são resolvidas de forma automática, ou quase isso. Nosso objetivo foi realmente entender como tudo se relaciona. Muitas dicas que você encontrou aqui foram descobertas testando, outras estudando, mas algumas não escaparam de serem descobertas apenas quando apareceu um problema. Aproveite!

Em um próximo post vamos falar de métricas baseadas em GPU e Horizontal Pod Auto Scaling. Ansioso por isso.

--

--