Nutanix Enterprise AI no NKPParte 4: CPU, GPU e MCP numa arquitetura híbrida
Testado emNKP 2.19Nutanix Enterprise AI 2.8.1Envoy AI GatewayLiteLLMVelero 1.18NDK 2.3.0Lab em Em outra versão, confira a matriz de compatibilidade e as release notes antes de aplicar.

Resumo: na Parte 3 o NAI respondeu o primeiro token em CPU. Agora ele deixa de ser uma ilha. Coloco o mesmo cliente OpenAI chamando o endpoint em CPU do NAI e um modelo de 30B numa GPU GB10, só trocando o
model. Ganho +40% de tokens/s em CPU com uma variável de ambiente. Levo as métricas do vLLM para o Grafana do NKP. Ligo o Agentic Gateway do NAI: unified endpoint, rate limit por tokens e MCP, validados de ponta a ponta com dois ajustes de configuração que mostro passo a passo. E protejo o NAI com Velero (cópia no Backblaze B2) e NDK (snapshot local), com restauração testada nos dois. Esta é a parte 4 de 4.
Onde estamos
| Parte | Tema |
|---|---|
| 1 | NAI: arquitetura, licenciamento e dimensionamento |
| 2 | Nutanix Files para o Kubernetes: o RWX que o NAI exige |
| 3 | Do catálogo ao primeiro token |
| 4 | CPU, GPU e MCP: o NAI numa arquitetura híbrida (este post) |
O ponto de partida: o NAI 2.8.1 num cluster NKP dedicado, um endpoint qwen3-06b-cpu (Qwen3 0,6B no vLLM, 8 vCPU) e, no mesmo lab, um ASUS Ascent GX10 (chip NVIDIA GB10, a plataforma do DGX Spark) servindo o Nemotron 30B por NIM atrás de um LiteLLM, que as aplicações do NKP já usavam.
O roteiro em uma tabela
| Etapa | O que entrega | Resultado no lab |
|---|---|---|
| 1. Mesma API, CPU e GPU | LiteLLM do GX10 roteando para o NAI | 71 tok/s na GPU, 5,5 tok/s na CPU, mesmo código |
| 2. Ajuste fino da CPU | VLLM_CPU_OMP_THREADS_BIND | +40% (5,5 → 7,3–8,0 tok/s) |
| 3. Observabilidade | PodMonitor + painel no Grafana do NKP | 349 séries vllm:* e o estrangulamento de CPU à vista |
| 4. Agentic Gateway | Unified endpoint, rate limit por tokens, MCP | 200 no unified endpoint, 429 na 11ª chamada, MCP de ponta a ponta |
| 5. Proteção | Velero → Backblaze B2 e NDK | Backup e restore testados nos dois |
1. A mesma API para CPU e GPU
O desenho é simples: o LiteLLM que já atendia as aplicações vira o ponto único. Cada aplicação tem uma chave do LiteLLM; o LiteLLM tem uma API key do NAI só para ele.
O que mudou no GX10
Três coisas, com backup e rollback:
- Uma API key nova no NAI (
litellm-gx10), só para o endpoint em CPU. A chave de teste continua separada. - O LiteLLM aprendeu a confiar na CA interna. O container usa o
certifido Python, que não conhece a CA do lab. Montei um bundle (as CAs docertifi+ a CA interna) e aponteiSSL_CERT_FILEeREQUESTS_CA_BUNDLEpara ele. Enquanto o nome do NAI não está no DNS, umextra_hostsresolve. - Um modelo novo no
config.yaml:
- model_name: qwen3-cpu-nai litellm_params: model: openai/qwen3-06b-cpu api_base: https://nai.lab.exemplo.local/enterpriseai/v1 api_key: os.environ/NAI_API_KEY extra_body: chat_template_kwargs: enable_thinking: falseO provider openai/ funciona porque a API do NAI é compatível com a da OpenAI. Só o container do LiteLLM foi recriado, e voltou em 11 segundos; o NIM nem percebeu.
O teste: o mesmo código, dois mundos
Um script Python com o SDK oficial da OpenAI, rodando num pod do cluster de aplicações do NKP. A única diferença entre as chamadas é o model:
client = OpenAI(base_url="https://gx10-llm.ai-gateway.svc/v1", api_key=os.environ["API_KEY"])for model in ["nemotron-lightning", "qwen3-cpu-nai"]: stream = client.chat.completions.create(model=model, stream=True, ...)| Modelo (backend) | TTFT | Tokens/s | ~100 tokens em |
|---|---|---|---|
nemotron-lightning (NIM 30B, GPU GB10) | 0,08–0,25 s | 71 | 1,5 s |
qwen3-cpu-nai (vLLM 0,6B, CPU, NAI) | 0,43–0,59 s | 5,5 | 14–16 s |
A GPU é 13× mais rápida com um modelo 50× maior. Ninguém vai usar o 0,6B em CPU para conversa; o ponto é outro: o código da aplicação não muda, e o log de uso do LiteLLM registra as chamadas dos dois backends no mesmo lugar.
Boa prática: libere o modelo novo em cada chave
A primeira chamada do pod ao modelo em CPU voltou 403: a chave do LiteLLM daquela aplicação estava restrita aos modelos da GPU. Liberar um backend novo é uma decisão por chave, não global, e é bom que seja assim. Ao adicionar um modelo no gateway, inclua-o também nas chaves das aplicações que vão usá-lo.
2. +40% em CPU com uma variável
Na Parte 3 o endpoint em CPU entregava 4,5 tokens/s e a explicação era o estrangulamento de CPU: o container enxerga os 12 núcleos do nó, o vLLM abre 11 threads OpenMP, mas o limite do pod é de 8 vCPU. O painel do Grafana (seção 3) mostrou o tamanho do efeito: 60% a 77% dos períodos estrangulados durante a geração.
O ajuste é a variável VLLM_CPU_OMP_THREADS_BIND=0-7, que prende as threads do vLLM em 8 núcleos. No NAI 2.8.1, a configuração avançada do endpoint oferece Engine Arguments e KV Cache Aware Routing; uma variável de ambiente como essa vai no InferenceService:

Apliquei a variável direto no InferenceService do KServe. Esse caminho fica fora da interface do NAI (em ambiente de produção, aja com cautela); no lab, o NAI manteve a mudança. O rollout pediu um ajuste: o predictor usa RollingUpdate, que sobe o pod novo antes de derrubar o antigo, e o nó de inferência do meu lab (12 vCPU) não comporta dois pods de 8 vCPU.
Endpoint maior que meio nó: use
Recreate
Sem folga para o surge, o pod novo fica Pending com Insufficient cpu e o autoscaler responde “max node group size reached”. O KServe aceita spec.predictor.deploymentStrategy: Recreate no InferenceService: com ele, o pod antigo sai primeiro. Para qualquer endpoint que ocupe mais da metade do nó, use Recreate ou deixe folga para o surge.
qwen3-cpu-nai (8 vCPU) | Tokens/s | TTFT | ~80 tokens em |
|---|---|---|---|
| Antes (11 threads OpenMP) | 5,3–5,7 | 0,43–0,59 s | 14–16,5 s |
Depois (VLLM_CPU_OMP_THREADS_BIND=0-7) | 7,3–8,0 | 0,29–0,43 s | 10,3–12,2 s |
+40% de tokens/s e 30% menos tempo total, sem mudar vCPU, memória nem modelo.
Faça uma chamada de aquecimento
A primeira requisição depois do restart levou 21,5 s até o primeiro token (o vLLM em CPU compila na primeira chamada). Depois de qualquer restart do endpoint, uma chamada descartável antes de liberar o tráfego.
3. As métricas do vLLM no Grafana do NKP
O NAI mostra requisições e tokens por endpoint. Para ver também latência e uso de CPU do pod, o vLLM expõe tudo isso em /metrics na porta 8000 do predictor; basta apontar o Prometheus do NKP para lá. Ele só coleta monitores com o rótulo do Kommander:
apiVersion: monitoring.coreos.com/v1kind: PodMonitormetadata: name: nai-vllm namespace: nai-admin labels: prometheus.kommander.d2iq.io/select: "true"spec: selector: matchLabels: endpoint.iep.nai.nutanix.com/engine: vllm podMetricsEndpoints: - portNumber: 8000 path: /metrics interval: 15sEm 10 segundos o alvo estava up e o Prometheus tinha 349 séries vllm:*. O painel vai por um ConfigMap com grafana_dashboard: "1", que o sidecar do Grafana do NKP carrega sozinho:

O painel que justificou a seção 2 é o de CPU do predictor e estrangulamento (CFS): durante a geração, o pod usava 5,5–6,3 vCPU e passava 60–77% dos períodos estrangulado.

O servidor confirma o cliente
A latência entre tokens no painel (p50 ~185 ms) equivale a ~5,4 tokens/s, o mesmo número medido pelo SDK. Quando cliente e servidor batem, o gargalo está no modelo, não na rede.
4. O Agentic Gateway
O NAI 2.8 trouxe o Agentic Gateway: unified endpoints (um nome só na frente de vários modelos, com balanceamento ou fallback), providers externos, rate limit por tokens e MCP. Por baixo está o Envoy AI Gateway. A licença do meu lab não inclui o Agentic Gateway (Parte 3), mas o menu já está na console, e usei o lab para conhecer o recurso a fundo.
Licenciamento
Tudo nesta seção foi feito num lab, para entender o produto. Em produção, inclua o Agentic Gateway na licença. As orientações de dimensionamento abaixo valem de qualquer jeito.

O desenho acima resume o que testei: chat e MCP entram pela mesma porta, e o gateway faz três coisas em sequência antes de chegar ao modelo ou ao MCP server. Ele é adaptado do fluxo do Agent Gateway que a Nutanix apresentou no lançamento do NAI 2.7.

Unified endpoint
Criei o unified-endpoint-pr, tipo Chat, com o endpoint local em CPU como modelo e a API key de teste:

A interface gerou um AIGatewayRoute do Envoy AI Gateway que casa o cabeçalho x-ai-eg-model: unified-endpoint-pr, troca o nome do modelo pelo do endpoint real e contabiliza tokens de entrada, saída e total. O caminho é /enterpriseai/gateway/v1/chat/completions; o /enterpriseai/v1, dos endpoints locais, devolve 404 para o nome do unified endpoint.
Faça antes: folga para o pod do gateway
Criar o primeiro unified endpoint faz o gateway subir um pod novo do proxy Envoy com o sidecar ai-gateway-extproc. O padrão reserva 3 vCPU para o envoy e 2 vCPU + 8 GiB para o extProc, por réplica, e o rollout precisa dessa folga num nó ao lado do pod atual. No meu lab, sem essa folga, o pod novo ficou Pending com Insufficient cpu e a primeira chamada voltou 500 sem corpo em 10 ms (não era a licença). Enquanto isso, o pod antigo continua servindo o resto do NAI. Antes do primeiro teste, confira se o pod novo do proxy está Running.
No lab, reduzi os requests (não os limites) no GatewayConfig e no EnvoyProxy do NAI. O pod novo subiu em 67 segundos, o antigo saiu sozinho, e o unified endpoint respondeu 200 em 5,4 s. Em produção, o caminho recomendado é dimensionar os nós do gateway para os valores padrão. São cerca de 5 vCPU e 10 GiB por réplica que entram na conta antes de qualquer modelo.
Rate limit por tokens
O rate limit é por tokens, não por requisições: global para o endpoint e, opcionalmente, por API key (ou por valor de um cabeçalho, quando várias aplicações dividem a chave).

Por baixo vira uma BackendTrafficPolicy do Envoy com duas regras. A de cliente identifica a chave pelo cabeçalho X-Nutanix-Api-Key-Id, que o próprio NAI injeta depois de autenticar, e o custo de cada requisição é o total de tokens da resposta. A rajada de teste, com 300 tokens/min para a chave:
| Chamada | HTTP | Tokens | x-ratelimit-remaining |
|---|---|---|---|
| 1 | 200 | 40 | 300 |
| 2 | 200 | 29 | 260 |
| 3 | 200 | 29 | 231 |
| … | 200 | 29–40 | … |
| 10 | 200 | 40 | 20 |
| 11 | 429 | — | 0 |
O contador desconta os tokens reais de cada resposta, e a 11ª chamada é barrada no gateway, em 13 ms, sem chegar ao modelo.
A chave precisa estar ligada ao endpoint
Uma API key válida para o endpoint local, mas não associada ao unified endpoint, recebe 401 “failed to validate API Key against multi-endpoint types”. O NAI controla o acesso por chave e por endpoint.
MCP: servers, connectors e controle de tools
A parte de MCP tem três peças:
- MCP Servers: locais (o NAI sobe a partir de uma imagem Docker) ou remotos (uma URL).
- MCP Connectors: juntam um ou mais servers e escolhem quais tools cada um expõe.
- MCP Client Keys: um tipo de chave próprio, separado das API keys de chat.
Usei um MCP server remoto público, o Context7 (documentação de bibliotecas). O connector descobriu as duas tools; liberar as duas exige digitar “confirm”:



O connector vira uma MCPRoute em /enterpriseai/gateway/mcp/<connector>, com um toolSelector listando as tools liberadas. É assim que o “Tool Control” da interface vira configuração.
MCP server com certificado P-384: inclua a curva no
Backend
Se o initialize pelo gateway voltar 500 “failed to create MCP session to any backend” e o log do extProc mostrar TLS_error … BAD_ECC_CERT, confira a curva do certificado do server. O do Context7 é ECDSA P-384, e o Backend criado para o server remoto usa as curvas padrão do Envoy (X25519 e P-256). Acrescentar ecdhCurves: [X25519, P-256, P-384, P-521], o SNI e as CAs do sistema no Backend resolveu.
apiVersion: gateway.envoyproxy.io/v1alpha1kind: Backendmetadata: name: mcp-context7 namespace: nai-admin-extensionsspec: type: Endpoints endpoints: - fqdn: { hostname: mcp.context7.com, port: 443 } tls: sni: mcp.context7.com wellKnownCACertificates: System ecdhCurves: [X25519, P-256, P-384, P-521]Com isso, o MCP pelo Agentic Gateway funcionou de ponta a ponta:
| Chamada MCP | HTTP | Tempo | Observação |
|---|---|---|---|
initialize | 200 | 0,77 s | Quem responde é o envoy-ai-gateway: o gateway é o servidor MCP para o cliente |
tools/list | 200 | 0,31 s | Tools prefixadas pelo server: mcp-context7__resolve-library-id |
tools/call | 200 | 1,44 s | Retornou as bibliotecas do Envoy Gateway |
O prefixo é o que permite juntar vários servers num connector sem conflito de nomes.
Valide o MCP server remoto antes de criar o connector
A descoberta de tools não passa pelo gateway: é o próprio nai-api que abre a sessão MCP com o server. No meu primeiro teste, com o DeepWiki, o passo notifications/initialized passou do tempo do nai-api (502 depois de 16 s) e a interface ficou em “Loading”. Com o Context7, no mesmo ambiente, a descoberta concluiu normalmente. Teste a sessão do seu server antes de levá-lo para produção.
5. Protegendo o NAI: Velero e NDK
O NAI guarda estado em quatro volumes RWO no nai-system (o banco PostgreSQL, o ClickHouse, o keeper e o Valkey) e os modelos em volumes RWX do Files. O banco roda em uma instância, então inclua esses volumes na sua rotina de backup. Usei as duas ferramentas do catálogo do NKP, com papéis diferentes.
Velero com cópia fora do lab (Backblaze B2)
O Velero do NKP 2.19 é o 1.18, com plugin AWS, data mover (Kopia) e node-agent. O destino foi um bucket no Backblaze B2, compatível com S3 e fora do lab. Duas notas para o B2: checksumAlgorithm: "" no backup storage location (o plugin novo manda um checksum que o B2 pode recusar) e o ciclo de vida do bucket em “manter só a última versão”, senão os backups apagados viram versões ocultas e o espaço só cresce.
Ao habilitar o Velero, a console avisa que falta o Rook Ceph Cluster:

Com destino S3 externo, o Rook Ceph não é necessário
Esse aviso vale para quem usa o Ceph local como destino. Com S3 externo, basta clicar em Enable. No meu lab eu habilitei o Rook mesmo assim: ele trouxe mons, mgrs e o pedido de 160 GiB de OSDs para um cluster já com 77–96% de CPU reservada, e um pod do NAI ficou Pending. Para desabilitar, a ordem importa: com o operador desabilitado antes do cluster, os recursos ficaram presos em finalizers. Se habilitar sem precisar, desabilite primeiro o Rook Ceph Cluster e só depois o Rook Ceph.
Outros dois detalhes do Velero no NKP:
- O pre-install espera o Ceph por 5 minutos. Com destino externo, o Velero sobe sozinho depois disso; não é erro.
- Depois de editar um override na console, confira a revisão do Helm. No meu teste, mudei o agendamento e o release continuou na revisão 1; conferir se o release subiu de revisão garante que a mudança chegou ao cluster.
No primeiro backup completo, o limite foi o link de saída do lab para o B2 (0,2–0,3 MB/s): o modelo de 1,5 GB levaria horas. Decisão: modelos fora do backup (voltam do Hugging Face em um minuto), com uma resourcePolicy que pula a storage class do Files:
version: v1volumePolicies: - conditions: storageClass: [nai-nfs] action: type: skipResultado: 510 objetos e 4 volumes em 776 s, 0 erros. Os ~710 MB dos volumes viraram ~313 MB no bucket (o Kopia comprime e deduplica):

Restore com data mover: suba um pod consumidor
A storage class do NKP é WaitForFirstConsumer, e o data mover baixa os dados quando algum pod usa o PVC. Na restauração de teste eu restaurei só os PVCs, sem pods, e ela ficou em “Accepted” por 8 minutos. Com um pod de verificação montando os volumes, o restore seguiu: banco de 632 MB baixado em 88 s (a descida do B2 é ~25× mais rápida que a subida), e o conteúdo conferido com donos e datas preservados.
NDK: snapshot local em segundos
O Nutanix Data Services for Kubernetes 2.3 protege a aplicação com snapshots do próprio storage Nutanix. A instalação pede uma credencial de Docker Hub própria do NDK (o portal Nutanix gera um token), passada no override:

O NDK é leve: 35m de CPU e 256 MiB de requests. Depois do Enable, três passos:
- StorageCluster: liga o NDK ao Prism Central e ao cluster AOS, pelos UUIDs dos dois. Peguei-os pela API v3 do Prism com a credencial que o CSI já tinha.
- Application: o namespace
nai-systeminteiro. O NDK pede um seletor explícito, e o seletor por namespace faz parte de um recurso que vem desligado por padrão (a mensagem é “multi namespace feature not enabled”). A forma simples de pegar o namespace inteiro é selecionar tudo que não tem um rótulo inexistente:
apiVersion: dataservices.nutanix.com/v1alpha1kind: Applicationmetadata: name: nai-system namespace: nai-systemspec: applicationSelector: resourceLabelSelectors: - labelSelector: matchExpressions: - { key: lab.exemplo.local/excluir, operator: DoesNotExist }- ApplicationSnapshot: pronto em 41 segundos, crash-consistent, com os deployments, statefulsets, secrets, configmaps e PVCs do NAI.
Não restaurei o NAI inteiro em outro namespace: subiria uma segunda cópia de 18 workloads num cluster de lab sem folga. Testei o mesmo mecanismo com uma aplicação pequena (1 volume com 50 MB aleatórios): snapshot em 37 s, restore em 21 s, hash idêntico. Para restaurar em outro namespace, o NDK exige um ReferenceGrant no namespace de origem.
Velero ou NDK? Os dois
| Velero → B2 | NDK | |
|---|---|---|
| Onde fica a cópia | Fora do site (objeto, Kopia) | Snapshot Nutanix local |
| Backup do NAI | 776 s (limitado pelo link) | 41 s |
| Restore testado | 4 volumes, dados conferidos | App de teste, 21 s, hash idêntico |
| Agenda | Diária, retenção de 14 dias | ProtectionPlan: a cada 6 h, retenção de 4 |
| Papel | Desastre: perdi o cluster ou o site | Erro operacional: volto em segundos, no lugar |
Os números
| Medida | Valor |
|---|---|
| GPU GB10, Nemotron 30B (NIM) | 71 tok/s, TTFT 0,08–0,25 s |
| CPU, Qwen3 0,6B (vLLM, 8 vCPU), antes / depois do ajuste | 5,5 → 7,3–8,0 tok/s (+40%) |
| Estrangulamento de CPU do predictor durante a geração (antes) | 60–77% dos períodos |
| Primeira requisição após restart (CPU) | 21,5 s até o primeiro token |
| Recursos do gateway com Agentic Gateway (padrão, por réplica) | ~5 vCPU e ~10 GiB |
| Rate limit (300 tokens/min por chave) | 10 chamadas aceitas, a 11ª barrada em 13 ms |
MCP pelo gateway (initialize / tools/list / tools/call) | 0,77 s / 0,31 s / 1,44 s |
| Velero: backup do NAI sem modelos | 510 objetos, 4 volumes, 776 s, ~313 MB no bucket |
NDK: snapshot do nai-system / restore de teste | 41 s / 21 s |
O que aprendi
- Um gateway na frente libera a aplicação da escolha do hardware. CPU para o barato, GPU para o pesado, o mesmo código.
- Em CPU, prenda as threads do vLLM ao limite do pod. Foram 40% de graça. E use
Recreateem endpoints maiores que meio nó. - Meça do lado do servidor. O PodMonitor do vLLM e o painel de estrangulamento mostraram o que nenhum número do cliente explicava.
- Reserve ~5 vCPU e ~10 GiB por réplica do Agentic Gateway antes de qualquer modelo. E confira se o pod novo do proxy está Running antes do primeiro teste do unified endpoint.
- Rate limit por tokens funciona como esperado, cobrando os tokens reais da resposta.
- Valide o MCP server remoto antes de criar o connector: a curva do certificado e a sessão MCP são os dois pontos a conferir.
- Velero e NDK não competem. Um tira a cópia do site; o outro volta em segundos.
- Restaurar volume com data mover exige um pod consumidor quando a storage class é
WaitForFirstConsumer.
Checklist: NAI híbrido e protegido
- API key dedicada no NAI para o gateway de LLM, separada da de teste
- CA interna no bundle de CAs do gateway (
SSL_CERT_FILE) - Chaves do gateway liberadas por modelo, por aplicação
-
VLLM_CPU_OMP_THREADS_BINDno endpoint em CPU edeploymentStrategy: Recreate - Chamada de aquecimento depois de qualquer restart do endpoint
- PodMonitor do vLLM com
prometheus.kommander.d2iq.io/select: "true"e painel no Grafana - Folga de ~5 vCPU e ~10 GiB por réplica do gateway antes de criar o primeiro unified endpoint
- API keys associadas a cada unified endpoint; MCP client keys separadas
- MCP server remoto testado (curva do certificado, sessão) antes de virar connector
- Velero com destino externo (sem Rook Ceph);
resourcePolicypara os modelos - Ciclo de vida do bucket em “última versão”
- Revisão do Helm conferida depois de cada Edit de override
- NDK com StorageCluster, Application do
nai-systeme ProtectionPlan - Restore testado nos dois
O que ainda não testei
- GPU dentro do próprio NKP. Nesta parte, a GPU entra pelo GX10, atrás do LiteLLM; um endpoint em GPU servido pelo NAI no NKP é o próximo passo do lab.
- Providers externos no Agentic Gateway. No lab, o unified endpoint roteou para o endpoint local em CPU e o MCP para o Context7; providers externos ficaram fora deste teste.
- O restore do NAI inteiro com o NDK em outro namespace. Subiria uma segunda cópia de 18 workloads num cluster de lab sem folga; testei o mesmo mecanismo com uma aplicação pequena.
Fim da série
Do catálogo ao MCP: o NAI saiu da decisão de arquitetura (Parte 1), ganhou o storage RWX (Parte 2), respondeu o primeiro token (Parte 3) e agora conversa com GPU, agentes, observabilidade e backup. O próximo passo do lab é a GPU dentro do próprio NKP.
Referências
Nutanix
- Nutanix Enterprise AI Guide 2.8.1 (portal Nutanix): endpoints, API keys, Agentic Gateway (unified endpoints, MCP servers e connectors, rate limit)
- Nutanix Data Services for Kubernetes 2.3 (portal Nutanix)
Projetos e comunidade
Comentários
Os comentários usam o GitHub Discussions; é preciso ter uma conta no GitHub.