🇧🇷 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.
- Visão Geral do Cluster
- Acesso e Autenticação
- Armazenamento
- Submissão de Jobs com Slurm
- Ambiente e Containers
- Exemplos Práticos
- Boas Práticas e Convivência
- Solução de Problemas
- Referência Rápida
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.
┌──────────────────────────────────────┐
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ávelSLURMD_NODENAMEquando 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.
Tudo abaixo foi lido do próprio nó, não de folha de dados.
| 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-fabricmanagerConsequê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.
| Item | Especificação |
|---|---|
| Processadores | 2× 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) |
| Item | Especificação |
|---|---|
| Scratch | /output — 28 TB ext4 |
| Dispositivo | md1 — RAID 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.
| Item | Estado |
|---|---|
| HCAs InfiniBand | 12 dispositivos mlx5_0…mlx5_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.
| 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.
O acesso é provisionado pela administração do cluster. Uma conta envolve três coisas que precisam existir em conjunto:
- Usuário do sistema (UID/GID) replicado nos dois nós, com
$HOMEem/home/<login>. - Associação Slurm ligando seu usuário a uma conta contábil e a uma QoS.
- 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,MaxJobsSaída típica: c4ai|seulogin|dgx|prioridade||| — conta c4ai, partição dgx, QoS
prioridade, sem limite de jobs simultâneos.
Todo acesso entra pelo nó de login:
ssh <seu_login>@192.168.1.100O 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 3Depois disso, ssh dgx basta. As diretivas ServerAliveInterval/Max evitam que
uma sessão longa caia em NAT ou firewall intermediário.
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.100Se 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çãoTokens 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/tokensOs scripts de job fazem
source ~/.secrets/tokense repassam ao container via-e.
A extensão Remote — SSH funciona diretamente contra o master:
- Instale Remote — SSH no VS Code local.
Ctrl+Shift+P→ Remote-SSH: Connect to Host →dgx(ou o host do seussh_config).- 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 aofiles.watcherExclude.
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,pip3econdaem/usr/local/binsã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.
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.
| 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) | só 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 login —ls /outputnomastermostra 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/$USERO 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:
- Nenhum container monta NFS. Todo bind mount aponta para
/output/.... - Diretórios de destino no NFS são criados pelo usuário do host, com
mkdir -p, antes dodocker run. - O NFS é tocado apenas por
rsyncexecutado 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_12345Quando exatamente o
rmdo 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 rmdo hostArquivo root:rootdentro de um diretório seuFunciona Subdiretório root:rootcom arquivos dentroFalha: Permission deniedOu 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 -rfna á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.
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
SIGTERMantes doSIGKILLao atingir o--time, respeitandoKillWait(30 s neste cluster). Para que otraptenha tempo de rodar orsync, 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 peloSIGKILL.Efeito colateral de
--signal: o job passa a terminar pelo sinal, então osacctregistraFAILEDcomExitCode 15:0em vez deTIMEOUT. É esperado — o15é oSIGTERM. Não confunda com falha real do seu código.
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.
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.csvO 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 jobsNunca coloque esse cache no NFS: o container grava nele e, se rodar como root, o
root_squash bloqueia.
/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 || trueSe 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'O Slurm é o único caminho para o nó de computação. Ele decide quem roda, quando e com quais recursos.
| 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 paradgx— simplesmente não peça--gres. Scripts herdados que especificam--partition=cpufalham na submissão.
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) |
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--timeherda 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.
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 7601O sbatch retorna assim que enfileira. Terminar sem erro no terminal não significa
que o job funcionou — sempre verifique com sacct (§4.9).
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 hostnameDentro 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.
srunbloqueia 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.
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 foundAs 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 -LConsequências práticas:
- Esquecer
--gres=gpu:Nnã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--gresprimeiro. - Você não precisa (e não deve) manipular
CUDA_VISIBLE_DEVICESpara escolher GPU. O Slurm já expõe somente as GPUs alocadas, sempre renumeradas a partir de0. - 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ãodevice 0dentro da task já é a correta. Se precisar do identificador global, useSLURM_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íticaArray 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%4Isso 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
.donepor 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
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.
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.
# 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 arrayO 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,ElapsedComportamentos 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ências2. 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"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.
| Nó | 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.
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:
- Monta os device nodes (
/dev/nvidia*) correspondentes às GPUs concedidas. - 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. - 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. ComConstrainDevices=yes, "all" significa "todas as GPUs que o Slurm alocou a este job" — não todas as do nó. Um job com--gres=gpu:1e--gpus allrecebe exatamente 1 GPU. Combinar a alocação do Slurm com--gpus allno Docker é mais robusto do que fixar índices à mão.
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())"'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 killE, 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.
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}" .
fiSem 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
RUNe não fica ativa no processo final. - Evite
CMD ["/bin/bash"]. UmCMDinterativo convida a rodar o container à mão com o projeto bind-montado — exatamente o que produz arquivosroot:rootno 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.
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-GPUAlternativamente, --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 topologiaDerive 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#!/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}"#!/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.
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.sbatchNotebook é ú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=/notebooks2. Leia o log para pegar a porta, o nó e o token:
cat logs/jupyter_<JOBID>.out3. 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.
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' | headSe aparecer root:root, o protocolo de I/O foi violado em algum ponto (§3.2) — corrija
o job em vez de apagar com sudo.
| Sintoma | Causa | Solução |
|---|---|---|
nvidia-smi: No devices were found |
Job sem --gres — ConstrainDevices=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) |
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 killDiretivas 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 bash— sem-l -
set -euo pipefail -
--timedeclarado explicitamente -
--gres=gpu:Nse usa GPU -
mkdir -pdos destinos NFS antes dodocker 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 otrapnão roda (§3.3.1) -
docker killpor label dentro dotrap— containers sobrevivem aoscancel -
--signal=B:SIGTERM@120se o stage-out é volumoso -
docker run --rm --user $(id -u):$(id -g) -
--shm-sizese usa DataLoader - Nenhum bind mount de
/homepara escrita - Limpeza de scratch órfão no início
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.