Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

Guia do Usuário — Cluster NVIDIA DGX H100

🇧🇷 Português (Brasil) · 🇬🇧 English version

Guia de referência para pesquisadores, cientistas de dados e engenheiros que executam treinamento de modelos, inferência e benchmarking no cluster DGX H100.

Este documento cobre exclusivamente a infraestrutura: hardware, acesso, escalonamento de jobs, containerização e armazenamento. Ele não documenta projetos ou pipelines específicos — as convenções aqui valem para qualquer carga de trabalho.

Todas as especificações foram verificadas diretamente no cluster em 6 de agosto de 2026. Valores que dependem de política administrativa (e não de hardware) estão sinalizados.


Sumário

  1. Visão Geral do Cluster
  2. Acesso e Autenticação
  3. Armazenamento
  4. Submissão de Jobs com Slurm
  5. Ambiente e Containers
  6. Exemplos Práticos
  7. Boas Práticas e Convivência
  8. Solução de Problemas
  9. Referência Rápida

1. Visão Geral do Cluster

O cluster é composto por dois nós com papéis distintos. Essa separação é a primeira coisa a internalizar: o nó onde você digita comandos não é o nó onde seu código roda.

1.1 Topologia

                    ┌──────────────────────────────────────┐
   você  ──ssh──▶   │  master  (nó de login)               │
                    │  ────────────────────────────────    │
                    │  • Sem GPU                           │
                    │  • Sem runtime NVIDIA no Docker      │
                    │  • Servidor NFS  (/export/home)      │
                    │  • Clientes Slurm (sbatch/srun/...)  │
                    │  • Edição de código, git, submissão  │
                    └───────────────┬──────────────────────┘
                                    │
                        Slurm  (sbatch / srun)
                                    │
                                    ▼
                    ┌──────────────────────────────────────┐
                    │  dgxh100-1  (nó de computação)       │
                    │  ────────────────────────────────    │
                    │  • 8× NVIDIA H100 80GB HBM3          │
                    │  • 4× NVSwitch on-board (NVLink)     │
                    │  • 224 CPUs · ~2 TiB RAM             │
                    │  • Docker COM runtime nvidia         │
                    │  • NVMe scratch  /output   (28 TB)   │
                    │  • NFS montado    /home    (29 TB)   │
                    └──────────────────────────────────────┘
Papel Host Endereço Função
Nó de login / master master 192.168.1.100 Acesso, edição, submissão, servidor NFS
Nó de computação dgxh100-1 Toda execução de carga de trabalho

O endereço do nó de computação não é divulgado. Ele não é endereçado diretamente por usuários, e você nunca precisa dele: o acesso é sempre pelo master, e o Slurm resolve o nó por nome. Dentro de um job, use a variável SLURMD_NODENAME quando precisar referenciá-lo (por exemplo, ao montar um túnel SSH — §6.4).

Cluster de nó único. Existe exatamente um nó de computação. Portanto #SBATCH -N 1 é sempre o valor correto, e paralelismo de pipeline entre nós (PIPELINE_PARALLEL_SIZE > 1) não se aplica hoje.

1.2 Especificações do nó de computação

Tudo abaixo foi lido do próprio nó, não de folha de dados.

GPU

Item Especificação
Modelo NVIDIA H100 SXM5 80GB HBM3 (PCI ID 10de:2330)
Quantidade 8
VRAM total 640 GB HBM3
Interconexão 4× NVSwitch on-board (PCI ID 10de:22a3) — malha NVLink de 4ª geração, todas-para-todas
Fabric Manager nvidia-fabricmanager ativo (obrigatório para o NVLink funcionar)
Recurso Slurm gpu:h100:8 — dispositivos /dev/nvidia[0-7]

Sobre os NVSwitch — o que eles são e o que não são. Os 4 NVSwitch deste nó são ASICs soldados na baseboard HGX H100, dentro do próprio chassi. Não são equipamento de rede, não ocupam espaço no rack e não aparecem como dispositivo separado — é por isso que sua existência costuma passar desapercebida. Eles são o que permite que qualquer par das 8 GPUs se comunique em largura de banda plena de NVLink, sem passar pelo PCIe.

Não confunda com o NVLink Switch System — os switches externos de rack que criam domínios NVLink entre múltiplos nós (usados em DGX SuperPOD e GB200 NVL72). Esses não existem aqui, e não fariam sentido num cluster de nó único.

Para auditar você mesmo, de dentro de um job:

lspci -nn | grep -i nvidia          # 4 linhas "H100 NVSwitch" + 8 "H100 SXM5 80GB"
systemctl is-active nvidia-fabricmanager

Consequência prática: paralelismo de tensor (TP) entre as 8 GPUs é barato neste nó — bem mais barato do que em nós PCIe com pontes parciais.

CPU e memória

Item Especificação
Processadores Intel Xeon Platinum 8480C
Núcleos físicos 112 (56 por socket)
CPUs lógicas 224 (SMT de 2 vias habilitado)
Nós NUMA 2
RAM 2.113.453.344 kB ≈ 2,01 TiB — Slurm expõe 2063919 MB
/dev/shm 1.008 GB (tmpfs)

Armazenamento local

Item Especificação
Scratch /output28 TB ext4
Dispositivo md1RAID 0 de 8× Samsung MZWLO3T8HCLS (3,84 TB NVMe U.2)
Chunk 512 KB

⚠️ RAID 0 não tem redundância. A falha de um único NVMe destrói os 28 TB. /output é área de trabalho volátil — nunca a única cópia de um resultado.

Rede

Item Estado
HCAs InfiniBand 12 dispositivos mlx5_0mlx5_11 presentes
Estado dos links todos DOWN — sem fabric InfiniBand ativo
Rede em uso Ethernet em rede privada interna

O hardware de IB existe e está instalado, mas não há fabric conectado. Isso não afeta nada hoje (cluster de nó único: comunicação entre GPUs é NVLink, não rede), e passa a importar se e quando houver expansão para múltiplos nós.

1.3 Pilha de software

Componente Versão Onde
Sistema operacional Ubuntu 24.04.4 LTS · kernel 6.8.0-101-generic ambos
Slurm 23.11.4 (slurm-wlm) ambos
Driver NVIDIA 580.126.09 (Open Kernel Module) dgxh100-1
Docker Engine 29.1.3 — runtimes: runc, nvidia dgxh100-1
Docker Engine 29.3.1 — runtimes: runc (sem nvidia) master
NVIDIA Container Toolkit 1.18.2 dgxh100-1

A diferença de runtimes entre os dois nós é a razão técnica pela qual cargas com GPU não podem rodar no master: o daemon de lá não sabe injetar dispositivos NVIDIA em um container, porque não há GPU alguma para injetar.


2. Acesso e Autenticação

2.1 Solicitação de conta

O acesso é provisionado pela administração do cluster. Uma conta envolve três coisas que precisam existir em conjunto:

  1. Usuário do sistema (UID/GID) replicado nos dois nós, com $HOME em /home/<login>.
  2. Associação Slurm ligando seu usuário a uma conta contábil e a uma QoS.
  3. Grupo docker, sem o qual você não consegue falar com o daemon no nó de computação.

As contas contábeis existentes são:

Conta Slurm Descrição
c4ai Conta institucional
pesquisadores Grupo geral de pesquisadores

Para conferir a sua própria associação depois de receber acesso:

sacctmgr show assoc where user=$(whoami) \
    format=Account,User,Partition,QOS,DefaultQOS,MaxJobs

Saída típica: c4ai|seulogin|dgx|prioridade||| — conta c4ai, partição dgx, QoS prioridade, sem limite de jobs simultâneos.

2.2 Conexão via SSH

Todo acesso entra pelo nó de login:

ssh <seu_login>@192.168.1.100

O nó de computação não recebe SSH direto de usuários; você chega nele através do Slurm. Isso é intencional — garante que todo consumo de recurso seja contabilizado e escalonado, em vez de disputado à mão.

Para conveniência, declare o host no seu ~/.ssh/config local:

Host dgx
    HostName 192.168.1.100
    User seu_login
    ServerAliveInterval 60
    ServerAliveCountMax 3

Depois disso, ssh dgx basta. As diretivas ServerAliveInterval/Max evitam que uma sessão longa caia em NAT ou firewall intermediário.

2.3 Chaves SSH

Prefira autenticação por chave. Na sua máquina local:

ssh-keygen -t ed25519 -C "seu_email@exemplo.br"
ssh-copy-id -i ~/.ssh/id_ed25519.pub seu_login@192.168.1.100

Se você for clonar repositórios privados de dentro do cluster, gere um par separado no cluster e registre a chave pública no provedor Git (GitHub/GitLab):

ssh-keygen -t ed25519 -C "login@dgx-cluster"
cat ~/.ssh/id_ed25519.pub          # registre este conteúdo no provedor
ssh -T git@github.com              # valide a autenticação

Tokens e segredos nunca ficam em repositório. A convenção do cluster é ~/.secrets/, com permissão restritiva:

mkdir -p ~/.secrets && chmod 700 ~/.secrets
printf 'export MINHA_API_TOKEN=...\n' > ~/.secrets/tokens
chmod 600 ~/.secrets/tokens

Os scripts de job fazem source ~/.secrets/tokens e repassam ao container via -e.

2.4 VS Code remoto

A extensão Remote — SSH funciona diretamente contra o master:

  1. Instale Remote — SSH no VS Code local.
  2. Ctrl+Shift+PRemote-SSH: Connect to Hostdgx (ou o host do seu ssh_config).
  3. Abra a pasta /home/<seu_login>/.

Você edita código com todo o conforto do editor, e continua submetendo pelo Slurm. O servidor do VS Code roda no nó de login — logo, o terminal integrado é o terminal do master, com todas as restrições da seção seguinte.

Cuidado com extensões pesadas. Extensões que indexam a árvore de arquivos (linters de projeto grande, Jupyter, ferramentas de busca semântica) percorrem o NFS e podem gerar carga significativa no nó de login. Adicione results/, data/ e diretórios de checkpoint ao files.watcherExclude.

2.5 O que nunca fazer no nó de login

O master é compartilhado por todos os usuários e não tem GPU. Ele existe para editar, compilar coisas pequenas, submeter e inspecionar.

❌ Não faça no master ✅ Faça assim
Treinar, avaliar ou inferir sbatch na partição dgx
python train.py Container via job Slurm
pip install / criar venv Dependências fixadas na imagem Docker
Descompactar dataset de 500 GB srun e trabalhar em /output
find recursivo em árvore gigante Manifesto pré-computado (§3.4)
Rodar container com GPU Não há runtime nvidia no master

Um processo pesado no nó de login degrada a experiência de todos, inclusive a capacidade de outras pessoas submeterem jobs.

A regra do Python é imposta tecnicamente. python, python3, pip, pip3 e conda em /usr/local/bin são symlinks para um script de bloqueio: para usuário comum eles recusam a execução com uma mensagem explicando o fluxo aprovado. Não é um lapso de instalação — é intencional. Rode Python dentro do container, via Slurm.


3. Armazenamento

Entender o storage deste cluster é o que separa um job que roda em horas de um que roda em dias. Há dois sistemas de arquivos com características opostas, e um mecanismo de segurança do NFS que dita o fluxo de dados obrigatório.

3.1 Os três sistemas de arquivos

Caminho Tipo Tamanho Visível em Persistência Velocidade
/home/<login> NFS4 (de 192.168.1.100:/export/home) 29 TB (14 TB livres) ambos os nós Persistente Lenta
/output/<login> ext4 sobre RAID 0 NVMe (md1) 28 TB (17 TB livres) dgxh100-1 Volátil Muito rápida
/ ext4 local 438 GB por nó Sistema

Como usar cada um:

  • /home — código, scripts, configurações, resultados finais, checkpoints que você quer guardar. É o único lugar com garantia de persistência e o único visível dos dois nós. É lento: latência de rede em cada operação de arquivo. Nunca faça I/O de treinamento diretamente aqui.
  • /output — área de trabalho do job. Dataset descompactado, cache de pesos, shards intermediários, cache de índice, saída em construção. Rápido porque é NVMe local em RAID 0. Não existe no nó de loginls /output no master mostra outra coisa (um diretório local homônimo), não o scratch de verdade.

Crie seu diretório de scratch na primeira vez que usar o nó:

srun --partition=dgx --time=2 mkdir -p /output/$USER

3.2 root_squash: a restrição que define o fluxo de I/O

O export NFS usa root_squash: um processo que se apresenta como root ao NFS é rebaixado para um usuário sem privilégios. Contêineres Docker, por padrão, rodam como root. A combinação produz uma falha que confunde muita gente:

container (root) ──escreve──▶ /home/...  ──▶  Permission denied

E há um efeito de segunda ordem: o próprio daemon do Docker não consegue montar caminhos sob /home neste cluster, pelo mesmo motivo.

Disso decorrem três regras que não são negociáveis:

  1. Nenhum container monta NFS. Todo bind mount aponta para /output/....
  2. Diretórios de destino no NFS são criados pelo usuário do host, com mkdir -p, antes do docker run.
  3. O NFS é tocado apenas por rsync executado pelo host, nunca pelo container.

Existem duas formas de evitar arquivos root:root. Use ambas quando possível:

# (a) O container roda com a sua identidade — o que ele cria já nasce com o dono certo
docker run --rm --user "$(id -u):$(id -g)" ...

# (b) Se o container precisa ser root (instalação em runtime, ferramenta que exige),
#     remova o que ele criou usando outro container:
docker run --rm -v /output/$USER:/scratch alpine:3 rm -rf /scratch/job_12345

Quando exatamente o rm do host falha. Um detalhe de POSIX que confunde: apagar um arquivo depende de permissão de escrita no diretório, não no arquivo. Verificado neste cluster:

Situação rm do host
Arquivo root:root dentro de um diretório seu Funciona
Subdiretório root:root com arquivos dentro Falha: Permission denied

Ou seja: o problema não é o dono do arquivo, é o dono do diretório. Como containers rodando como root normalmente criam subdiretórios, um rm -rf na árvore falha no meio — e é aí que a remoção via container (opção b) se torna necessária.

A opção (a) é a preferida: nada nasce root:root, e o stage-out fica trivial. O custo é que o container não tem CAP_DAC_OVERRIDE — ele depende de que os diretórios de destino já existam com o UID correto, o que reforça a regra 2.

3.3 O protocolo stage-in / stage-out

Este é o padrão de I/O do cluster. Todo job que lê ou escreve volume de dados relevante deve seguí-lo:

[host]        mkdir -p ${NFS_OUT}              # pré-criar destino (root_squash)
[host]        rsync -a  NFS  ──▶  /output      # stage-in
[container]   processa lendo/gravando em /output   # rápido, sem root_squash
[host]        rsync -a  /output  ──▶  NFS      # stage-out (SEMPRE)
[host]        remove o scratch                 # limpeza

A parte mais importante — e a mais esquecida — é que o stage-out precisa acontecer mesmo em falha. Se o container quebrar no meio, se o job estourar o time limit ou se alguém der scancel, o trabalho já feito ainda está no scratch e deve chegar ao NFS. Um trap resolve:

Escrever o trap é necessário, mas não é suficiente. Há duas armadilhas neste cluster que só aparecem em teste, e ambas fazem o trap virar decoração. Este é o padrão correto — cada detalhe dele foi verificado empiricamente (§3.3.1):

#!/usr/bin/env bash
set -euo pipefail

SCRATCH="/output/${USER}/job_${SLURM_JOB_ID}"
NFS_OUT="${HOME}/resultados/run_${SLURM_JOB_ID}"

mkdir -p "${SCRATCH}"/{entrada,saida} "${NFS_OUT}"   # destino NFS criado pelo HOST

STAGED=0
stage_out() {
    [[ ${STAGED} -eq 1 ]] && return 0    # idempotente: SIGTERM e EXIT disparam ambos
    STAGED=1
    # Mata o container: ele é filho do daemon Docker, não do cgroup do job,
    # e por isso SOBREVIVE ao scancel se não for morto explicitamente.
    docker ps -q --filter "label=slurm_job=${SLURM_JOB_ID}" | xargs -r docker kill >/dev/null 2>&1 || true
    echo "[stage-out] ${SCRATCH} -> ${NFS_OUT}"
    rsync -a "${SCRATCH}/saida/" "${NFS_OUT}/" || echo "[WARN] stage-out parcial"
}
trap stage_out EXIT SIGTERM SIGINT

rsync -a "${HOME}/dados/" "${SCRATCH}/entrada/"      # stage-in

# O container roda em BACKGROUND e o script espera com `wait`.
# Em foreground, o bash adia o handler e o trap NUNCA roda em scancel/timeout.
docker run --rm \
    --name "job-${SLURM_JOB_ID}" \
    --label "slurm_job=${SLURM_JOB_ID}" \
    --user "$(id -u):$(id -g)" \
    -v "${SCRATCH}:/scratch" \
    "${IMAGE}" processa /scratch/entrada /scratch/saida &
CONTAINER_PID=$!

RC=0
wait "${CONTAINER_PID}" || RC=$?         # `wait` retorna de imediato ao receber sinal
echo "[INFO] container terminou com código ${RC}"
exit "${RC}"

Sinais e time limit. O Slurm envia SIGTERM antes do SIGKILL ao atingir o --time, respeitando KillWait (30 s neste cluster). Para que o trap tenha tempo de rodar o rsync, peça aviso antecipado com #SBATCH --signal=B:SIGTERM@120 (120 s antes do fim). Sem isso, um stage-out de muitos GB pode ser interrompido pelo SIGKILL.

Efeito colateral de --signal: o job passa a terminar pelo sinal, então o sacct registra FAILED com ExitCode 15:0 em vez de TIMEOUT. É esperado — o 15 é o SIGTERM. Não confunda com falha real do seu código.

3.3.1 Por que background + wait: o teste

As duas armadilhas acima não são teóricas. Submetendo dois jobs idênticos exceto pela forma de invocar o container, e dando scancel em ambos:

Invocação do container trap disparou? Trabalho parcial no NFS Container após o scancel
docker run ... (foreground) Não Perdido Sobreviveu órfão
docker run ... & + wait Sim Preservado Morto pelo trap

O motivo do primeiro caso é uma regra do próprio bash: enquanto o shell espera um comando em foreground, um sinal com trap definido fica pendente e o handler só roda quando o comando termina — o que, sob scancel, não acontece a tempo. Com o container em background, o wait retorna imediatamente ao receber o sinal e o handler roda.

O segundo caso é consequência da arquitetura do Docker: os processos do container são filhos do daemon, não do cgroup do job. Matar o job não os alcança. Verificado: um container seguia Up 48 seconds depois de o job ter sido cancelado. Num job de GPU, isso deixa memória de GPU presa enquanto o Slurm considera a GPU livre — e o próximo job falha.

3.4 Datasets volumosos: padrões que escalam

Três padrões fazem a diferença entre um pipeline que aguenta milhões de arquivos e um que trava.

1. Nunca find no NFS dentro de um array job. Se 100 tasks executam find na mesma árvore NFS, você multiplicou por 100 uma operação já lenta. Em vez disso, calcule um manifesto CSV uma única vez e fatie por índice dentro de cada task:

# Uma vez, em um job dedicado:
find "${DATA_ROOT}" -name '*.parquet' | sort | nl -ba -s, > manifest.csv

# Dentro de cada task do array — O(1) em relação ao tamanho do dataset:
START=$(( SLURM_ARRAY_TASK_ID * BATCH_SIZE ))
END=$((   START + BATCH_SIZE - 1 ))
awk -v s=$((START + 1)) -v e=$((END + 1)) 'NR >= s && NR <= e' manifest.csv > slice.csv

O custo por task deixa de crescer com o tamanho da coleção. A mesma estrutura serve para mil ou dez milhões de arquivos, sem mudar código.

2. Poucos arquivos grandes, não muitos pequenos. NFS paga latência por arquivo. Um milhão de arquivos de 4 KB é dramaticamente pior que mil shards de 4 MB. Ao gerar dados intermediários, prefira formatos shardados (.jsonl, .parquet, .tar, webdataset) e mova poucos arquivos grandes entre NFS e scratch.

3. Idempotência com marcadores. Jobs longos morrem. Grave um marcador .done por unidade de trabalho concluída, e verifique-o antes de reprocessar:

MARKER="${NFS_OUT}/.done/task_${SLURM_ARRAY_TASK_ID}.done"
if [[ -f "${MARKER}" ]]; then
    echo "[SKIP] task já concluída"; exit 0
fi
# ... processamento ...
mkdir -p "$(dirname "${MARKER}")" && touch "${MARKER}"

Assim, reenviar o array depois de uma falha reprocessa apenas o que faltava.

4. Cache compartilhado de pesos e modelos. Downloads de modelos (HuggingFace e similares) devem apontar para o scratch, e o diretório pode ser reaproveitado entre jobs, porque o scratch persiste entre execuções neste nó:

export HF_HOME=/output/$USER/hf_cache      # baixa uma vez, reusa em todos os jobs

Nunca coloque esse cache no NFS: o container grava nele e, se rodar como root, o root_squash bloqueia.

3.5 Higiene de scratch

/output é finito (28 TB) e compartilhado entre todos os usuários. Jobs que morrem deixam scratch órfão. Duas práticas mantêm o espaço saudável:

Limpeza no início de cada job. Antes de começar, remova seus scratches antigos sem modificação há mais de 60 minutos:

find /output/$USER -maxdepth 1 -type d -name 'job_*' -mmin +60 \
     -exec rm -rf {} + 2>/dev/null || true

Se o container rodou como root e criou subdiretórios, esse rm -rf falha no meio com Permission denied (§3.2) — nesse caso, remova via container:

docker run --rm -v /output/$USER:/scratch alpine:3 \
    sh -c 'find /scratch -maxdepth 1 -name "job_*" -mmin +60 -exec rm -rf {} +'

Nunca apague scratch de job que ainda está na fila. O mtime de um scratch congela ao fim do stage-in, então um critério baseado só em idade apaga o trabalho de tasks em execução. O critério correto é o job ter saído da fila:

# Lista de jobs seus ainda ativos — preserve os scratches correspondentes
squeue -u "$USER" -h -o '%A'

4. Submissão de Jobs com Slurm

O Slurm é o único caminho para o nó de computação. Ele decide quem roda, quando e com quais recursos.

4.1 Conceitos e a partição única

Termo Significado
Job Uma solicitação de recursos + o que executar
Partição Fila lógica sobre um conjunto de nós
Alocação Recursos concedidos (CPUs, RAM, GPUs) por um tempo
Array job Um job que se expande em N tasks quase idênticas
QoS Classe de serviço que modula prioridade e limites

Este cluster tem exatamente uma partição:

$ sinfo -o "%20P %5a %10l %6D %10T %N"
PARTITION            AVAIL TIMELIMIT  NODES  STATE      NODELIST
dgx*                 up    infinite   1      mixed      dgxh100-1
Propriedade Valor
Nome dgx (o * indica que é a partição padrão)
Nós dgxh100-1
MaxTime UNLIMITED
DefaultTime NONE
OverSubscribe NO
TRES cpu=224, mem=2063919M, gres/gpu=8

Não existe partição cpu. Jobs sem GPU também vão para dgx — simplesmente não peça --gres. Scripts herdados que especificam --partition=cpu falham na submissão.

4.2 Como pedir recursos

Peça o que vai usar. Sub-pedir causa OOM e lentidão; super-pedir bloqueia colegas.

Diretiva Efeito Recomendação
-N 1 Número de nós Sempre 1 (cluster de nó único)
--gres=gpu:N N GPUs 1 para inferência/extração · até 4 para treino distribuído
-c N / --cpus-per-task CPUs lógicas 8–24 por GPU; 32 é bom para treino multi-GPU
--mem=NG RAM Peça o que precisa. --mem=0 = toda a RAM do nó
--time=D-HH:MM:SS Limite de tempo Sempre defina (veja o alerta abaixo)

Limites de política (administrativos)

O Slurm deste cluster não impõe cotas de GPU nem de tempo — as QoS não têm MaxTRES e a partição é UNLIMITED. Os limites abaixo são política da administração, anunciada no MOTD a cada login, e valem por convenção:

Limite Valor Acima disso
GPUs por usuário 4 Solicitar à administração
Duração de um job 4 dias Solicitar à administração

Como nada disso é imposto tecnicamente, respeitá-lo é o que mantém o cluster utilizável para os demais — e, na prática, é comum ver a fila bloqueada justamente por jobs que os ignoram. Os exemplos deste guia usam no máximo 4 GPUs.

⚠️ DefaultTime=NONE + MaxTime=UNLIMITED. Um job submetido sem --time herda tempo ilimitado. Isso parece conveniente e é, na verdade, o principal problema de convivência do cluster: um job assim ocupa o recurso indefinidamente e o escalonador de backfill não consegue encaixar nada nos intervalos, porque não sabe quando ele termina. Sempre declare --time, com folga honesta.

Sobre CPUs: o nó tem 112 núcleos físicos com SMT de 2 vias = 224 CPUs lógicas. O Slurm usa SelectType=cons_tres com CR_CORE, ou seja, aloca por núcleo. Pedir -c 32 consome 16 núcleos físicos (32 threads).

Sobre memória: DefMemPerNode=UNLIMITED, então um job sem --mem não fica limitado por padrão. Ainda assim, declare — é o que permite ao escalonador raciocinar sobre o nó. Para treinos grandes, --mem=0 (toda a RAM) é o idiomático.

4.3 Jobs em lote (sbatch)

Template comentado, incorporando todas as convenções do cluster:

#!/usr/bin/env bash
# ATENÇÃO: shebang SEM -l. Ver §4.10 — um login shell corrompe o exit status.
#SBATCH --job-name=meu-treino
#SBATCH --partition=dgx
#SBATCH --nodes=1
#SBATCH --gres=gpu:4
#SBATCH --cpus-per-task=32
#SBATCH --mem=0                       # toda a RAM do nó
#SBATCH --time=2-00:00:00             # 2 dias — SEMPRE declare
#SBATCH --signal=B:SIGTERM@120        # avisa 120s antes do fim (para o stage-out)
#SBATCH --output=logs/%x_%j.out       # %x=job-name  %j=job-id
#SBATCH --error=logs/%x_%j.err

set -euo pipefail

# --- Configuração: zero valores hardcoded -----------------------------------
PROJECT_DIR="${SLURM_SUBMIT_DIR}"
source "${PROJECT_DIR}/config.env"

IMAGE="${IMAGE:?defina IMAGE em config.env}"
SCRATCH="/output/${USER}/${SLURM_JOB_NAME}_${SLURM_JOB_ID}"
NFS_OUT="${PROJECT_DIR}/results/run_$(date +%Y%m%d)_${SLURM_JOB_ID}"

# --- Diretórios criados pelo HOST (root_squash) ------------------------------
mkdir -p "${SCRATCH}"/{entrada,saida} "${NFS_OUT}" "${PROJECT_DIR}/logs"

# --- Limpeza de scratch órfão ------------------------------------------------
find "/output/${USER}" -maxdepth 1 -type d -mmin +60 -name "${SLURM_JOB_NAME}_*" \
     -exec rm -rf {} + 2>/dev/null || true

# --- Stage-out garantido, inclusive em falha (ver §3.3) ----------------------
STAGED=0
stage_out() {
    [[ ${STAGED} -eq 1 ]] && return 0
    STAGED=1
    docker ps -q --filter "label=slurm_job=${SLURM_JOB_ID}" | xargs -r docker kill >/dev/null 2>&1 || true
    rsync -a "${SCRATCH}/saida/" "${NFS_OUT}/" || echo "[WARN] stage-out parcial"
}
trap stage_out EXIT SIGTERM SIGINT

# --- Stage-in ----------------------------------------------------------------
rsync -a "${PROJECT_DIR}/data/" "${SCRATCH}/entrada/"

# --- Execução (background + wait: obrigatório para o trap funcionar) ---------
docker run --rm \
    --name "meu-treino-${SLURM_JOB_ID}" \
    --label "slurm_job=${SLURM_JOB_ID}" \
    --user "$(id -u):$(id -g)" \
    --gpus all \
    --shm-size=64g \
    -v "${SCRATCH}:/scratch" \
    -e OMP_NUM_THREADS="${SLURM_CPUS_PER_TASK}" \
    "${IMAGE}" \
    torchrun --nproc_per_node=8 /workspace/train.py \
        --data /scratch/entrada --out /scratch/saida &
CONTAINER_PID=$!

RC=0
wait "${CONTAINER_PID}" || RC=$?
echo "[INFO] job ${SLURM_JOB_ID} — container saiu com código ${RC}"
exit "${RC}"

Submissão e resposta:

$ sbatch slurm/treino.sbatch
Submitted batch job 7601

O sbatch retorna assim que enfileira. Terminar sem erro no terminal não significa que o job funcionou — sempre verifique com sacct (§4.9).

4.4 Jobs interativos (srun)

Para depurar, inspecionar o nó ou rodar algo curto:

# Shell interativo com 1 GPU por 1 hora
srun --partition=dgx --gres=gpu:1 --cpus-per-task=8 --mem=64G \
     --time=01:00:00 --pty bash

# Comando único, sem shell
srun --partition=dgx --gres=gpu:1 --time=00:05:00 nvidia-smi

# Verificação rápida CPU-only (entra na fila quase sempre na hora)
srun --partition=dgx --cpus-per-task=1 --mem=1G --time=00:02:00 hostname

Dentro de uma sessão srun --pty, você está no dgxh100-1: enxerga /output de verdade, tem o daemon Docker com runtime nvidia e vê as GPUs que foram alocadas.

srun bloqueia até haver recurso. Se as 8 GPUs estiverem ocupadas, um pedido de GPU fica pendurado. Um pedido CPU-only normalmente entra de imediato, porque há 224 CPUs e os jobs de treino usam poucas dezenas.

4.5 Alocação de GPU

Este é o ponto que mais gera confusão, e há um comportamento verificado que vale memorizar. A configuração de cgroup do cluster é:

ConstrainCores=yes
ConstrainRAMSpace=yes
ConstrainDevices=yes      ◀── isolamento de dispositivos

ConstrainDevices=yes significa que as GPUs simplesmente não existem para um job que não as pediu. Comprovação empírica no nó — este é o comando e a saída real:

$ srun --partition=dgx --cpus-per-task=1 --time=2 nvidia-smi
No devices were found

As 8 GPUs estão fisicamente ali, o driver está carregado, e ainda assim o job não vê nenhuma. Peça uma GPU e ela aparece:

srun --partition=dgx --gres=gpu:1 --time=2 nvidia-smi -L

Consequências práticas:

  • Esquecer --gres=gpu:N não gera erro claro: seu código cai em CPU e roda ordens de magnitude mais lento, ou falha com uma mensagem obscura de CUDA. Se o treino está inexplicavelmente lento, verifique o --gres primeiro.
  • Você não precisa (e não deve) manipular CUDA_VISIBLE_DEVICES para escolher GPU. O Slurm já expõe somente as GPUs alocadas, sempre renumeradas a partir de 0.
  • Em array jobs, nunca derive a GPU de SLURM_ARRAY_TASK_ID % 8. Use a que o Slurm reservou. Com o cgroup ativo, a GPU alocada é a única visível, então device 0 dentro da task já é a correta. Se precisar do identificador global, use SLURM_JOB_GPUS.

Formas de pedir:

--gres=gpu:1            # 1 GPU qualquer
--gres=gpu:4            # 4 — o teto de política por usuário
--gres=gpu:h100:4       # 4, especificando o tipo declarado no gres.conf
--gres=gpu:8            # sintaticamente válido, mas acima do limite de política

4.6 Array jobs

Array jobs são a forma correta de processar N unidades independentes: um documento por task, um modelo por task, uma partição de dados por task.

#SBATCH --array=0-999%4

Isso cria 1000 tasks e mantém no máximo 4 rodando ao mesmo tempo — o %4 é o regulador de paralelismo. Para tasks de 1 GPU, ele é também o que mantém você dentro do limite de 4 GPUs por usuário; para tasks CPU-only você pode subir esse número.

Limite deste cluster Valor
MaxArraySize 1001 (índices de 0 a 1000)
MaxJobCount 10000

Para mais de 1001 unidades, agrupe: cada task processa um lote de BATCH_SIZE itens recortados do manifesto (§3.4). Uma coleção de 500 mil documentos vira 1000 tasks de 500 itens cada.

Variáveis disponíveis dentro de uma task:

Variável Conteúdo
SLURM_ARRAY_JOB_ID ID do array como um todo
SLURM_ARRAY_TASK_ID Índice desta task
SLURM_ARRAY_TASK_COUNT Total de tasks
SLURM_JOB_GPUS GPUs efetivamente alocadas a esta task

Boas práticas específicas de array:

  • Throttle sempre (%N). Sem ele, o Slurm tenta encaixar o máximo possível e a pressão sobre o NVMe e a rede degrada todas as tasks.

  • Um marcador .done por task (§3.4), para reenviar só o que falhou.

  • Serialize efeitos colaterais globais. Se todas as tasks podem precisar construir a mesma imagem Docker, use flock, senão você dispara 8 builds idênticos:

    exec 9>/output/$USER/.build.lock
    flock 9                     # a primeira task builda; as outras esperam e reusam
    docker image inspect "${IMAGE}" &>/dev/null || docker build -t "${IMAGE}" .
    flock -u 9

4.7 Dependências entre jobs

Pipelines de múltiplos estágios se encadeiam sem supervisão humana:

JOB1=$(sbatch --parsable slurm/preparo.sbatch)
JOB2=$(sbatch --parsable --dependency=afterok:${JOB1} slurm/treino.sbatch)
JOB3=$(sbatch --parsable --dependency=afterany:${JOB2} slurm/consolida.sbatch)
Tipo Dispara quando
afterok:ID O job terminou com sucesso
afterany:ID O job terminou, com sucesso ou não
afternotok:ID O job falhou
after:ID O job começou

afterok para o caminho normal; afterany para recuperação e consolidação — é justamente quando o job anterior falhou que você quer resgatar o que sobrou.

4.8 Prioridade e QoS

O escalonador usa priority/multifactor com sched/backfill. QoS é um dos fatores, e este cluster define quatro níveis:

QoS Prioridade Flags
normal 0
pesquisadores 10
prioridade 100
ilimitado 1000 PartitionTimeLimit

A QoS de cada usuário é atribuída pela administração via associação Slurm — não é autosserviço. A flag PartitionTimeLimit na ilimitado permite exceder o limite de tempo da partição.

O backfill é o motivo pelo qual declarar --time corretamente beneficia você também: se o escalonador sabe que seu job leva 30 minutos, ele o encaixa em uma janela livre antes de um job grande e pendente. Sem --time, seu job só roda quando a fila esvazia.

4.9 Monitoramento

# Fila: seus jobs
squeue -u "$USER"

# Fila completa, com GPUs e tempo — visão útil de contenção
squeue -o "%.8i %.12u %.12q %.6D %.20b %.11M %.11l %.8T %R"

# Status final de um job (a verificação que importa)
sacct -j <JOBID> --format=JobID,JobName,State,ExitCode,Elapsed,MaxRSS

# Detalhes completos de um job na fila
scontrol show job <JOBID>

# Log em tempo real
tail -f logs/meu-treino_<JOBID>.out

# Cancelar
scancel <JOBID>
scancel -u "$USER"                    # todos os seus
scancel <ARRAY_JOB_ID>_<TASK_ID>      # uma task específica do array

O estado desejado em sacct é COMPLETED com ExitCode 0:0. Estados a reconhecer:

Estado Significado
PENDING Aguardando recurso — veja a coluna REASON no squeue
RUNNING Em execução
COMPLETED Sucesso, exit 0
FAILED Exit diferente de zero
TIMEOUT Estourou o --time
OUT_OF_MEMORY OOM — aumente --mem
NODE_FAIL Falha do nó — resubmeta

Para saber quanta memória um job realmente usou (e calibrar o --mem da próxima vez):

sacct -j <JOBID> --format=JobID,MaxRSS,MaxVMSize,ReqMem,Elapsed

4.10 Particularidades deste cluster

Comportamentos verificados que não estão na documentação genérica do Slurm.

1. Shebang sem -l — obrigatório. Neste cluster, um script de job com #!/bin/bash -l termina com status 1 mesmo em exit 0. O efeito é traiçoeiro: toda execução bem-sucedida aparece como FAILED no sacct, e qualquer encadeamento por --dependency=afterok nunca dispara. Use sempre:

#!/usr/bin/env bash        # ✅ correto
#!/bin/bash -l             # ❌ quebra o exit status e as dependências

2. SLURM_SUBMIT_DIR é o diretório de submissão. O job não começa necessariamente onde você espera. Derive caminhos dele, não de pwd, e submeta da raiz do projeto.

3. GPUs invisíveis sem --gres — §4.5.

4. A partição dgx é a padrão. --partition=dgx é redundante, mas escrevê-lo explicitamente documenta a intenção e protege contra mudanças futuras de configuração.

5. Um job pode ocupar as 8 GPUs por tempo indeterminado. Com MaxTime=UNLIMITED e uma QoS ilimitado disponível, a fila pode ficar bloqueada por muitas horas. Verifique a contenção antes de planejar seu dia:

squeue -o "%.8i %.12u %.12q %.20b %.11M %.11l %.8T"

5. Ambiente e Containers

5.1 Por que containers

Neste cluster, todo código de aplicação roda dentro de um container. Não há pip install no bare-metal, não há ambientes virtuais fora de imagens, não há python script.py direto no nó.

As razões são concretas:

  • Reprodutibilidade. A imagem fixa versões de CUDA, cuDNN, PyTorch e bibliotecas. Um resultado de seis meses atrás continua reproduzível.
  • Isolamento. Usuários com stacks incompatíveis convivem no mesmo nó sem conflito.
  • Nada de mutação do nó. O nó de computação não acumula pacotes instalados por dezenas de pessoas ao longo do tempo.
  • Estabilidade do nó de login. Builds e execuções acontecem onde devem.

5.2 Onde o Docker existe

Docker Runtime nvidia Uso
master 29.3.1 ❌ ausente Nenhum trabalho — sem GPU
dgxh100-1 29.1.3 ✅ presente Toda execução

O daemon com runtime NVIDIA existe apenas no nó de computação, acessível através do Slurm. Consequência: docker build e docker run acontecem dentro de um job, não no nó de login.

Para inspecionar imagens ou depurar um container manualmente, entre no nó primeiro:

srun --partition=dgx --pty --time=00:30:00 bash
docker images
docker ps --filter "label=slurm_job=${JOBID}"

Você precisa pertencer ao grupo docker (§2.1). Confirme com id | grep docker.

5.3 Como o container conversa com o driver NVIDIA

Este é o mecanismo que o usuário pediu para entender, e vale detalhar porque explica quase todos os erros de "CUDA não encontrado".

O driver NVIDIA é um módulo de kernel — ele vive no host e não pode ser containerizado. A imagem, por sua vez, carrega o espaço de usuário do CUDA (a runtime, cuDNN, NCCL, bibliotecas). A ponte entre os dois é o NVIDIA Container Toolkit (v1.18.2 aqui):

┌─────────────────────────────────────────────────────────────┐
│ CONTAINER                                                   │
│   sua aplicação  (PyTorch / JAX / TensorRT)                 │
│   CUDA runtime + cuDNN + NCCL      ◀── vem da IMAGEM        │
│ ─────────────────────────────────────────────────────────── │
│   libcuda.so, libnvidia-ml.so      ◀── INJETADOS do host    │
│   /dev/nvidia0..7, /dev/nvidiactl  ◀── INJETADOS do host    │
└──────────────────────────┬──────────────────────────────────┘
                           │  nvidia-container-toolkit 1.18.2
┌──────────────────────────▼──────────────────────────────────┐
│ HOST  dgxh100-1                                             │
│   Driver NVIDIA 580.126.09  (módulo de kernel)              │
│   8× H100 SXM5 80GB HBM3  +  4× NVSwitch on-board           │
└─────────────────────────────────────────────────────────────┘

Quando você passa --gpus ao docker run, o toolkit intercepta a criação do container e:

  1. Monta os device nodes (/dev/nvidia*) correspondentes às GPUs concedidas.
  2. Injeta as bibliotecas de driver do host (libcuda.so, libnvidia-ml.so, …) — e é por isso que a versão do driver dentro do container é sempre a do host, 580.126.09.
  3. Disponibiliza utilitários como o nvidia-smi.

A imagem não deve conter um driver. Ela contém a runtime CUDA; o driver vem do host. Confundir os dois é a origem clássica do erro Failed to initialize NVML: Driver/library version mismatch.

Como pedir GPUs ao Docker:

docker run --rm --gpus all              ...   # todas as GPUs visíveis ao job
docker run --rm --gpus '"device=0,1"'   ...   # subconjunto, por índice

--gpus all é seguro e é o idiomático aqui. Com ConstrainDevices=yes, "all" significa "todas as GPUs que o Slurm alocou a este job" — não todas as do nó. Um job com --gres=gpu:1 e --gpus all recebe exatamente 1 GPU. Combinar a alocação do Slurm com --gpus all no Docker é mais robusto do que fixar índices à mão.

5.4 Compatibilidade CUDA

Driver do host: 580.126.09 (ramo CUDA 13.x).

CUDA na imagem Funciona? Observação
11.x Compatibilidade retroativa do driver
12.x Caso mais comum hoje
13.0 Ramo nativo deste driver
> 13.0 ⚠️ Exigiria pacote de forward compatibility

A regra geral é que o driver suporta qualquer runtime CUDA igual ou anterior ao seu ramo. Como o driver aqui é bem recente, praticamente todas as imagens oficiais em circulação (NGC, PyTorch, TensorFlow) funcionam sem ajuste.

Para verificar o que a sua imagem realmente vê:

srun --partition=dgx --gres=gpu:1 --time=00:05:00 \
    docker run --rm --gpus all "${IMAGE}" \
    bash -c 'nvidia-smi; python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.device_count())"'

5.5 Anatomia de um docker run correto

Cada opção abaixo existe por um motivo específico deste cluster:

docker run --rm \
    --name "${PROJETO}-${PAPEL}-${SLURM_JOB_ID}" \
    --label "project=${PROJETO}" \
    --label "slurm_job=${SLURM_JOB_ID}" \
    --label "role=${PAPEL}" \
    --user "$(id -u):$(id -g)" \
    --gpus all \
    --shm-size=64g \
    -v "/output/${USER}/job_${SLURM_JOB_ID}:/scratch" \
    -e OMP_NUM_THREADS="${SLURM_CPUS_PER_TASK}" \
    -e HF_HOME=/scratch/hf_cache \
    "${IMAGE}" \
    python /workspace/app.py --data /scratch/entrada
Opção Por quê
--rm Nunca deixe container parado. Eles acumulam e consomem disco do nó
--name com SLURM_JOB_ID No docker ps, cada container é rastreável ao job. Sem isso, você vê nomes aleatórios e não sabe o que matar
--label Permite kill seletivo e auditoria (veja abaixo)
--user $(id -u):$(id -g) Arquivos nascem com o dono correto, não root:root (§3.2)
--gpus all GPUs alocadas pelo Slurm (§5.3)
--shm-size=64g O default de 64 MB quebra DataLoader do PyTorch (§5.7)
-v .../output/... Só scratch. Nunca monte /home (§3.2)
-e OMP_NUM_THREADS Sem isso, bibliotecas BLAS abrem 224 threads e brigam entre si

Uma convenção de nome que funciona bem, por ser autoexplicativa no docker ps:

{projeto}-{papel}-{JOBID}[-t{TASK_ID}][-p{FASE}]

treino-cpt-7601
extracao-ocr-7620-t3
avaliacao-bench-7655-t12-p2

Com labels, você mata só o que precisa — inclusive de dentro de um trap:

docker ps -q --filter "label=slurm_job=${SLURM_JOB_ID}" | xargs -r docker kill

E, por segurança, se o container executa código não confiável (por exemplo, avaliação de código gerado por modelo):

docker run --rm --cap-drop=ALL --security-opt=no-new-privileges ...

Note que isso não remove acesso à rede nem aos volumes montados — é redução de privilégio, não sandbox completo.

5.6 Construção de imagens (lazy build)

Não há registry central. As imagens são construídas localmente no nó de computação e persistem entre jobs. O padrão é construir sob demanda, dentro do próprio job:

lazy_build() {
    local image="$1" context="$2"
    if docker image inspect "${image}" &>/dev/null; then
        echo "[SKIP] imagem ${image} já existe"
        return 0
    fi
    echo "[BUILD] construindo ${image}"
    docker build -t "${image}" "${context}"
}

lazy_build "minha-imagem:latest" "${PROJECT_DIR}"

Assim, o primeiro job constrói (alguns minutos) e todos os seguintes reaproveitam. Nenhum passo manual de build é necessário.

Uma melhoria que evita uma classe inteira de bugs: decidir o rebuild por conteúdo, não por tag. Grave um fingerprint do Dockerfile e das dependências como label da imagem, e compare:

FINGERPRINT=$(cat Dockerfile requirements.txt | sha256sum | cut -c1-16)
ATUAL=$(docker image inspect "${IMAGE}" \
        --format '{{ index .Config.Labels "fingerprint" }}' 2>/dev/null || echo "")

if [[ "${ATUAL}" != "${FINGERPRINT}" ]]; then
    echo "[BUILD] imagem desatualizada (${ATUAL} != ${FINGERPRINT})"
    docker build --label "fingerprint=${FINGERPRINT}" -t "${IMAGE}" .
fi

Sem isso, editar o Dockerfile sem trocar a tag reaproveita silenciosamente a imagem antiga — e você depura um código que não está rodando.

Recomendações para os Dockerfiles:

  • Fixe versões em requirements.txt. Nunca instale nada em runtime dentro do job.
  • Não crie venv dentro da imagem. O container já é o ambiente isolado; um venv adiciona uma camada que não sobrevive entre instruções RUN e não fica ativa no processo final.
  • Evite CMD ["/bin/bash"]. Um CMD interativo convida a rodar o container à mão com o projeto bind-montado — exatamente o que produz arquivos root:root no NFS. Prefira um entrypoint explícito, invocado com argumentos pelo script de job.
  • Ordene as camadas por volatilidade: primeiro a base e o sistema, depois as dependências, e o código da aplicação por último.

5.7 Memória compartilhada e NCCL

O Docker aloca 64 MB de /dev/shm por padrão. É pouco demais para cargas de deep learning, e a falha é confusa: o DataLoader do PyTorch morre com Bus error ou DataLoader worker (pid X) is killed by signal.

O host tem 1.008 GB de /dev/shm. Use uma fatia generosa:

--shm-size=64g          # bom ponto de partida para treino multi-GPU

Alternativamente, --ipc=host compartilha o namespace de IPC do host — resolve o mesmo problema, com menos isolamento.

Para treinamento distribuído nas GPUs alocadas, o NCCL roteia pela malha NVLink/NVSwitch automaticamente; não há nada a configurar no caso comum. Como não há fabric InfiniBand ativo (§1.2), desabilitar a busca por IB elimina um timeout inicial:

-e NCCL_IB_DISABLE=1              # sem fabric IB neste cluster
-e NCCL_DEBUG=WARN                # ou INFO, para diagnosticar topologia

Derive o número de processos das GPUs alocadas, em vez de fixar 8 — o mesmo script passa a funcionar com qualquer --gres:

NUM_GPUS=$(nvidia-smi -L | wc -l)
torchrun --nproc_per_node="${NUM_GPUS}" train.py

6. Exemplos Práticos

6.1 Treinamento multi-GPU

#!/usr/bin/env bash
#SBATCH --job-name=treino
#SBATCH --partition=dgx
#SBATCH --nodes=1
#SBATCH --gres=gpu:4
#SBATCH --cpus-per-task=32
#SBATCH --mem=0
#SBATCH --time=3-00:00:00
#SBATCH --signal=B:SIGTERM@180
#SBATCH --output=logs/%x_%j.out
#SBATCH --error=logs/%x_%j.err

set -euo pipefail

IMAGE="treino:latest"
SCRATCH="/output/${USER}/treino_${SLURM_JOB_ID}"
NFS_OUT="${SLURM_SUBMIT_DIR}/results/run_$(date +%Y%m%d)_${SLURM_JOB_ID}"

mkdir -p "${SCRATCH}"/{data,ckpt} "${NFS_OUT}" "${SLURM_SUBMIT_DIR}/logs"

# Stage-out garantido: checkpoints chegam ao NFS mesmo em timeout ou crash (§3.3)
STAGED=0
stage_out() {
    [[ ${STAGED} -eq 1 ]] && return 0
    STAGED=1
    docker ps -q --filter "label=slurm_job=${SLURM_JOB_ID}" | xargs -r docker kill >/dev/null 2>&1 || true
    echo "[stage-out] preservando checkpoints"
    rsync -a "${SCRATCH}/ckpt/" "${NFS_OUT}/checkpoints/" || echo "[WARN] parcial"
}
trap stage_out EXIT SIGTERM SIGINT

rsync -a "${SLURM_SUBMIT_DIR}/data/processed/" "${SCRATCH}/data/"

docker image inspect "${IMAGE}" &>/dev/null || \
    docker build -t "${IMAGE}" "${SLURM_SUBMIT_DIR}"

NUM_GPUS=$(nvidia-smi -L | wc -l)
echo "[INFO] treinando em ${NUM_GPUS} GPU(s)"

docker run --rm \
    --name "treino-cpt-${SLURM_JOB_ID}" \
    --label "slurm_job=${SLURM_JOB_ID}" --label "role=treino" \
    --user "$(id -u):$(id -g)" \
    --gpus all \
    --shm-size=64g \
    -v "${SCRATCH}:/scratch" \
    -e OMP_NUM_THREADS=4 \
    -e NCCL_IB_DISABLE=1 \
    "${IMAGE}" \
    torchrun --nproc_per_node="${NUM_GPUS}" /workspace/train.py \
        --data /scratch/data --ckpt-dir /scratch/ckpt &
CONTAINER_PID=$!

RC=0
wait "${CONTAINER_PID}" || RC=$?
echo "[INFO] treino terminou com código ${RC}"
exit "${RC}"

6.2 Inferência em 1 GPU

#!/usr/bin/env bash
#SBATCH --job-name=inferencia
#SBATCH --partition=dgx
#SBATCH --gres=gpu:1
#SBATCH --cpus-per-task=8
#SBATCH --mem=64G
#SBATCH --time=04:00:00
#SBATCH --output=logs/%x_%j.out

set -euo pipefail

SCRATCH="/output/${USER}/inf_${SLURM_JOB_ID}"
mkdir -p "${SCRATCH}/out" "/output/${USER}/hf_cache"

mkdir -p "${SLURM_SUBMIT_DIR}/results/${SLURM_JOB_ID}"

STAGED=0
stage_out() {
    [[ ${STAGED} -eq 1 ]] && return 0; STAGED=1
    docker ps -q --filter "label=slurm_job=${SLURM_JOB_ID}" | xargs -r docker kill >/dev/null 2>&1 || true
    rsync -a "${SCRATCH}/out/" "${SLURM_SUBMIT_DIR}/results/${SLURM_JOB_ID}/" || true
}
trap stage_out EXIT SIGTERM SIGINT

docker run --rm \
    --name "inferencia-${SLURM_JOB_ID}" \
    --label "slurm_job=${SLURM_JOB_ID}" \
    --user "$(id -u):$(id -g)" \
    --gpus all --shm-size=16g \
    -v "${SCRATCH}:/scratch" \
    -v "/output/${USER}/hf_cache:/hf_cache" \
    -e HF_HOME=/hf_cache \
    "inferencia:latest" \
    python /workspace/infer.py --out /scratch/out &
CONTAINER_PID=$!

RC=0
wait "${CONTAINER_PID}" || RC=$?
exit "${RC}"

O cache de pesos em /output/$USER/hf_cache é montado fora do scratch do job, então sobrevive à limpeza e é reaproveitado por todos os jobs seguintes.

6.3 Array job CPU-only

Processamento de 500 mil itens em 1000 tasks de 500, 16 tasks simultâneas:

#!/usr/bin/env bash
#SBATCH --job-name=processa
#SBATCH --partition=dgx
#SBATCH --array=0-999%16
#SBATCH --cpus-per-task=8
#SBATCH --mem=32G
#SBATCH --time=04:00:00
#SBATCH --output=logs/%x_%A_%a.out

set -euo pipefail

BATCH_SIZE=500
MANIFEST="${SLURM_SUBMIT_DIR}/manifests/itens.csv"
NFS_OUT="${SLURM_SUBMIT_DIR}/results/array_${SLURM_ARRAY_JOB_ID}"
MARKER="${NFS_OUT}/.done/task_${SLURM_ARRAY_TASK_ID}.done"

# Idempotência: reenviar o array reprocessa só o que faltou
if [[ -f "${MARKER}" ]]; then
    echo "[SKIP] task ${SLURM_ARRAY_TASK_ID} já concluída"; exit 0
fi

SCRATCH="/output/${USER}/proc_${SLURM_ARRAY_JOB_ID}_t${SLURM_ARRAY_TASK_ID}"
mkdir -p "${SCRATCH}"/{in,out} "${NFS_OUT}/dados" "${NFS_OUT}/.done"

STAGED=0
stage_out() {
    [[ ${STAGED} -eq 1 ]] && return 0; STAGED=1
    docker ps -q --filter "label=slurm_job=${SLURM_ARRAY_JOB_ID}" \
        --filter "name=processa-${SLURM_ARRAY_JOB_ID}-t${SLURM_ARRAY_TASK_ID}" \
        | xargs -r docker kill >/dev/null 2>&1 || true
    rsync -a "${SCRATCH}/out/" "${NFS_OUT}/dados/" || true
}
trap stage_out EXIT SIGTERM SIGINT

# Fatia do manifesto — custo O(1) em relação ao tamanho da coleção
START=$(( SLURM_ARRAY_TASK_ID * BATCH_SIZE ))
awk -v s=$((START + 2)) -v e=$((START + BATCH_SIZE + 1)) \
    'NR >= s && NR <= e' "${MANIFEST}" > "${SCRATCH}/in/slice.csv"

if [[ ! -s "${SCRATCH}/in/slice.csv" ]]; then
    echo "[AVISO] nenhum item para esta task — array maior que o manifesto"; exit 0
fi

docker run --rm \
    --name "processa-${SLURM_ARRAY_JOB_ID}-t${SLURM_ARRAY_TASK_ID}" \
    --label "slurm_job=${SLURM_ARRAY_JOB_ID}" \
    --user "$(id -u):$(id -g)" \
    -v "${SCRATCH}:/scratch" \
    -e NUM_WORKERS="${SLURM_CPUS_PER_TASK}" \
    "processa:latest" \
    python /workspace/run.py --manifest /scratch/in/slice.csv --out /scratch/out &
CONTAINER_PID=$!

RC=0
wait "${CONTAINER_PID}" || RC=$?

# O marcador .done só é gravado se a task realmente teve sucesso
[[ ${RC} -eq 0 ]] && touch "${MARKER}"
exit "${RC}"

Submissão com cálculo automático do tamanho do array:

TOTAL=$(( $(wc -l < manifests/itens.csv) - 1 ))     # -1 pelo cabeçalho
N_TASKS=$(( (TOTAL + 500 - 1) / 500 - 1 ))
[[ ${N_TASKS} -gt 1000 ]] && { echo "excede MaxArraySize=1001"; exit 1; }
sbatch --array=0-${N_TASKS}%16 slurm/processa.sbatch

6.4 Jupyter Notebook com GPU

Notebook é útil para exploração, mas ocupa GPU enquanto estiver aberto. Use --time curto e feche ao terminar.

1. Submeta o servidor:

#!/usr/bin/env bash
#SBATCH --job-name=jupyter
#SBATCH --partition=dgx
#SBATCH --gres=gpu:1
#SBATCH --cpus-per-task=8
#SBATCH --mem=64G
#SBATCH --time=04:00:00
#SBATCH --output=logs/jupyter_%j.out

set -euo pipefail
PORT=$(shuf -i 20000-29999 -n 1)

echo "==================================================="
echo " Túnel SSH (execute na SUA máquina):"
echo "   ssh -N -L ${PORT}:${SLURMD_NODENAME}:${PORT} ${USER}@192.168.1.100"
echo " Depois abra:  http://localhost:${PORT}"
echo "==================================================="

mkdir -p "/output/${USER}/jupyter"

docker run --rm \
    --name "jupyter-${SLURM_JOB_ID}" \
    --user "$(id -u):$(id -g)" \
    --gpus all --shm-size=16g \
    --network host \
    -v "/output/${USER}/jupyter:/scratch" \
    -v "${HOME}/notebooks:/notebooks:ro" \
    "jupyter:latest" \
    jupyter lab --no-browser --ip=0.0.0.0 --port="${PORT}" --notebook-dir=/notebooks

2. Leia o log para pegar a porta, o nó e o token:

cat logs/jupyter_<JOBID>.out

3. Abra o túnel na sua máquina local, com o comando que o log imprimiu, e acesse http://localhost:<PORT>.

O -v "${HOME}/notebooks:/notebooks:ro" é uma exceção deliberada à regra de não montar NFS: somente leitura, arquivos pequenos, e o container roda com o seu UID. Escreva resultados em /scratch, nunca no volume NFS.


7. Boas Práticas e Convivência

O cluster tem um nó e 8 GPUs compartilhados por cerca de uma dúzia de usuários. A eficiência coletiva depende mais de disciplina do que de configuração.

Declare --time sempre, e com honestidade. É a regra de maior impacto. Como DefaultTime=NONE, omitir significa "ilimitado", o que impede o backfill de encaixar jobs curtos nas janelas livres. Um --time realista faz o seu job entrar mais cedo.

Peça só as GPUs que vai usar. Um job de 1 GPU pedindo --gres=gpu:4 deixa 3 H100 ociosas. Se o seu código não é distribuído, peça 1.

Faça checkpoint com frequência. Jobs morrem: timeout, OOM, falha de nó, scancel. Sem checkpoint, dias de computação evaporam. Combine checkpoint frequente no scratch com o trap de stage-out (§3.3) para que a última versão sempre chegue ao NFS.

Verifique a fila antes de planejar. Um job com QoS ilimitado usando as 8 GPUs pode estar rodando há 15 horas sem previsão de término:

squeue -o "%.8i %.12u %.12q %.20b %.11M %.11l %.8T"

Nunca trabalhe no nó de login (§2.5).

Limpe o que você deixou. Scratches órfãos e containers parados consomem recursos de todos. A limpeza automática no início de cada job (§3.5) resolve o caso comum.

Um smoke test antes de um job grande. Rode com --time=00:10:00, um subconjunto minúsculo de dados e poucos passos. Descobrir um erro de path depois de 20 horas de fila é frustrante e caro.

Não confie no silêncio do sbatch. Ele retorna ao enfileirar. Confirme com sacct -j <JOBID> --format=JobID,State,ExitCode e verifique se a saída esperada tem o dono correto:

find "${NFS_OUT}" -name '*.json' -printf '%u:%g  %p\n' | head

Se aparecer root:root, o protocolo de I/O foi violado em algum ponto (§3.2) — corrija o job em vez de apagar com sudo.


8. Solução de Problemas

Sintoma Causa Solução
nvidia-smi: No devices were found Job sem --gresConstrainDevices=yes esconde as GPUs Adicione --gres=gpu:N (§4.5)
Treino inexplicavelmente lento Rodando em CPU por falta de --gres scancel e resubmeta com --gres
Failed to initialize NVML: Driver/library version mismatch Imagem tentando fornecer o próprio driver A imagem deve ter só a runtime CUDA; o driver vem do host (§5.3)
Permission denied ao gravar no NFS Container root + root_squash Pré-crie o diretório no host; grave no scratch e faça stage-out (§3.2)
Arquivos de saída como root:root Container rodou como root Use --user $(id -u):$(id -g) (§3.2)
docker: Cannot connect to the Docker daemon Fora do nó de computação, ou fora do grupo docker Rode via Slurm; confirme com id | grep docker (§5.2)
GPU não aparece no container Falta --gpus no docker run Adicione --gpus all (§5.3)
Bus error / DataLoader worker killed /dev/shm default de 64 MB --shm-size=64g ou --ipc=host (§5.7)
Job COMPLETED mas nada no NFS Stage-out não executou Use trap ... EXIT SIGTERM SIGINT (§3.3)
trap definido mas não roda em scancel/timeout Container em foreground — o bash adia o handler Rode o container em background + wait (§3.3.1)
GPU ocupada sem job correspondente na fila Container órfão sobreviveu ao scancel Mate por label no trap (§3.3); resgate: docker ps -q --filter label=slurm_job=<ID> | xargs -r docker kill
sacct mostra FAILED 15:0 em vez de TIMEOUT --signal=B:SIGTERM@N encerrou o job pelo sinal Esperado — 15 é SIGTERM, não falha do seu código (§3.3)
Job TIMEOUT e resultados perdidos Sinal de aviso não configurado --signal=B:SIGTERM@120 + trap (§3.3)
Todo job aparece FAILED mesmo com exit 0 Shebang #!/bin/bash -l Use #!/usr/bin/env bash (§4.10)
--dependency=afterok nunca dispara Mesma causa acima Idem
sbatch: Invalid partition name Script pedindo partição inexistente (ex.: cpu) Só existe dgx (§4.1)
sbatch: array acima do limite Array > 1001 tasks Agrupe em lotes maiores por task (§4.6)
OUT_OF_MEMORY no sacct --mem insuficiente Meça com sacct --format=MaxRSS e aumente
PENDING por muito tempo GPUs ocupadas Veja REASON no squeue; verifique a contenção (§4.10)
Nenhum espaço em /output Scratch órfão acumulado Limpeza (§3.5) — preserve jobs ainda na fila
rm: Permission denied no scratch Arquivos root-owned Remova via container (§3.2)
Timeout inicial longo no NCCL Busca por fabric InfiniBand inexistente -e NCCL_IB_DISABLE=1 (§5.7)
Editei o Dockerfile e nada mudou lazy_build reusou a imagem pela tag Rebuild por fingerprint de conteúdo (§5.6)
Nó de login lento para todos Alguém processando no master Nunca trabalhe no login (§2.5)

9. Referência Rápida

Hardware

master        nó de login · sem GPU · servidor NFS       · 192.168.1.100
dgxh100-1     8× H100 SXM5 80GB HBM3 · 4× NVSwitch on-board
              2× Xeon 8480C · 112 núcleos / 224 CPUs · ~2 TiB RAM  · acesso só via Slurm
/home         NFS4  · 29 TB · persistente · lento  · visível nos 2 nós
/output       NVMe RAID 0 · 28 TB · volátil · rápido · só no nó de computação
Driver 580.126.09 · Docker 29.1.3 (+nvidia) · Toolkit 1.18.2 · Slurm 23.11.4

Comandos essenciais

# Submeter e monitorar
sbatch job.sbatch                       # enfileirar
squeue -u $USER                         # meus jobs
sacct -j <ID> --format=JobID,State,ExitCode,Elapsed,MaxRSS
scancel <ID>                            # cancelar
scontrol show job <ID>                  # detalhes

# Interativo
srun -p dgx --gres=gpu:1 -c 8 --mem=64G --time=1:00:00 --pty bash
srun -p dgx --time=2 nvidia-smi         # (sem --gres: "No devices were found")

# Cluster
sinfo -o "%20P %5a %10l %6D %10T %N"    # partições
scontrol show node dgxh100-1            # recursos do nó
sacctmgr show assoc where user=$USER format=Account,QOS

# Docker (no nó de computação)
docker ps --filter "label=slurm_job=<ID>"
docker ps -q --filter "label=slurm_job=<ID>" | xargs -r docker kill

Diretivas de referência por tipo de job

Tipo Diretivas
Treino distribuído --gres=gpu:4 -c 32 --mem=0 --time=3-00:00:00
Treino / inferência 1 GPU --gres=gpu:1 -c 8 --mem=64G --time=04:00:00
Array com GPU --array=0-999%4 --gres=gpu:1 -c 8 --mem=32G
CPU leve -c 4 --mem=16G --time=02:00:00
CPU pesado (paralelo) -c 64 --mem=512G --time=12:00:00

Checklist de todo sbatch

  • #!/usr/bin/env bashsem -l
  • set -euo pipefail
  • --time declarado explicitamente
  • --gres=gpu:N se usa GPU
  • mkdir -p dos destinos NFS antes do docker run
  • Stage-in para /output, processamento no scratch
  • trap stage_out EXIT SIGTERM SIGINT, com guarda de idempotência
  • Container em background + wait — sem isso o trap não roda (§3.3.1)
  • docker kill por label dentro do trap — containers sobrevivem ao scancel
  • --signal=B:SIGTERM@120 se o stage-out é volumoso
  • docker run --rm --user $(id -u):$(id -g)
  • --shm-size se usa DataLoader
  • Nenhum bind mount de /home para escrita
  • Limpeza de scratch órfão no início

Convenções deste documento

Especificações de hardware, versões e parâmetros do Slurm foram lidos diretamente do cluster (sinfo, scontrol, sacctmgr, /proc, lsblk, docker info) em 6 de agosto de 2026. Comportamentos marcados como verificado foram reproduzidos empiricamente. Políticas administrativas — atribuição de QoS, provisionamento de contas — podem mudar sem alteração de hardware; confirme com a administração em caso de dúvida.

Correções e melhorias são bem-vindas via issue ou pull request.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors