O Pathways oferece benefícios de resiliência das seguintes maneiras:
- Suspender e retomar: permite que sua carga de trabalho tolere interrupções planejadas, como avisos de preempção, sem exigir um código personalizado de processamento de preempção.
- Treinamento elástico: permite que sua carga de trabalho tolere falhas de hardware não planejadas sem causar falhas no cliente usando a lógica de recuperação no código de treinamento.
Antes de começar
Antes de começar, faça o seguinte:
- Configurar o Cluster Toolkit
- Instalar ferramentas do Kubernetes
- Instale a CLI do Google Cloud
- Ativar a API Google Kubernetes Engine
Suspender e retomar
O Google Kubernetes Engine (GKE) normalmente envia um aviso de preempção para um pod acelerador antes que ele seja interrompido. A tolerância de preempção do Pathways é ativada por padrão em todas as implantações na nuvem, e os jobs do acelerador do Pathways detectam esses avisos.
Quando um aviso de preempção chega, o Pathways primeiro determina se pode salvar e restaurar a carga de trabalho de forma transparente. Se sim, ele tenta suspender de forma transparente a carga de trabalho de ML gravando o estado atual dela em um armazenamento permanente, como o Cloud Storage, antes que o GKE remova os jobs do acelerador. Quando o GKE reprogramar os jobs mais tarde, o Pathways vai retomar a carga de trabalho de ML lendo o estado persistente dela.
Se a carga de trabalho não for restaurável, o Pathways vai encerrar o job do acelerador e encaminhar a falha para o job se o treinamento elástico estiver configurado. Se o treinamento elástico não estiver configurado, o GKE vai reiniciar toda a carga de trabalho com base na política de reinicialização do JobSet.
As cargas de trabalho típicas de ML definidas usando o JAX dependem de componentes XLA do Pathways sem estado, que podem ser restaurados usando um snapshot de memória de alta largura de banda (HBM, na sigla em inglês). Algumas cargas de trabalho de ML, como as definidas usando a API Python colocalizada do JAX, dependem de componentes do Pathways com estado. Essas cargas de trabalho não são restauráveis.
Treinamento elástico
O treinamento elástico permite que o job de treinamento continue quando ocorrem falhas de hardware ou preempções de fração, sem reiniciar toda a carga de trabalho. Em um job de treinamento de várias frações, o Pathways mantém o processo do cliente de controlador único e as frações de TPU íntegras ativas quando uma fração individual fica indisponível.
O treinamento elástico combina os recursos do sistema Pathways com a lógica de recuperação no seu código de treinamento:
- Detecção de falhas: quando ocorre uma falha de hardware ou preempção em uma fração de TPU, o Pathways cancela a atribuição da fração afetada e gera um
jax.errors.JaxRuntimeErrorna próxima vez que sua carga de trabalho acessar dados nessa fração. Como o processo do cliente permanece ativo, seu código pode detectar esse erro, reconfigurar os recursos e continuar o treinamento. - Modos de recuperação: a classe
pathwaysutils.elastic.manager.Managere o decoradorelastic_retryoferecem suporte a duas estratégias de recuperação:- Pausar e retomar: quando
minimum_slice_countforNoneou igual ao número total de frações no job, o treinamento será pausado quando uma fração for perdida e aguardará até que todas as frações estejam íntegras novamente antes de ser retomado. Como a contagem de frações não muda entre as tentativas, sua carga de trabalho pode reutilizar a malha de dispositivos e os executáveis compilados. - Redimensionamento de réplica: quando
minimum_slice_counté menor que o número total de frações, o treinamento continua imediatamente nas frações ativas restantes reconstruindo a malha do dispositivo e recompilando para a topologia menor. Uma linha de execução de monitoramento em segundo plano verifica periodicamente as fatias inativas e registra as fatias recém-íntegras emelastic_manager.available_inactive_slices. O loop de treinamento pode recriar a malha do dispositivo com as fatias de substituição, reinicializar para a topologia expandida e colocar o estado atual na nova malha.
- Pausar e retomar: quando
- Recuperação de estado: para retomar o treinamento após um evento elástico, sua carga de trabalho
restaura os parâmetros do modelo, o estado do otimizador e o estado do iterador de dados usando
snapshots na memória. A classe
Snapshotteremorbax.checkpointcopia de forma assíncrona o estado replicado da HBM da TPU para a memória da CPUpinned_hostem cada fração. Durante a recuperação, oSnapshotterreconstrói o estado a partir de réplicas de host sobreviventes e o refragmenta diretamente na malha de TPU ativa sem ler do armazenamento remoto.
Ativar a elasticidade no servidor proxy do Pathways
Para ativar conjuntos de dispositivos elásticos no Pathways no GKE, adicione a
flag --num_elastic_instances ao campo args do contêiner pathways-proxy
no manifesto do JobSet e especifique o número de frações de TPU elásticas
na carga de trabalho:
- name: pathways-proxy
image: us-docker.pkg.dev/cloud-tpu-v2-images/pathways/proxy_server:latest
args:
- --resource_manager_address=$(PATHWAYS_HEAD):29001
- --server_port=29000
- --gcs_scratch_location=gs://BUCKET_NAME
- --num_elastic_instances=NUM_SLICES
Substitua:
BUCKET_NAME: o bucket do Cloud Storage usado para arquivos temporários do Pathways.NUM_SLICES: o número de frações de TPU na carga de trabalho.
Filtrar dispositivos ativos e criar a malha de dispositivos
Em uma carga de trabalho do Pathways, jax.devices() retorna todos os dispositivos na alocação inicial durante o ciclo de vida do processo do cliente. Ao usar o treinamento elástico, use elastic_manager.active_devices para que a malha do dispositivo e o tamanho do lote usem apenas frações íntegras e ativas:
import jax
import numpy as np
from pathwaysutils.elastic import manager
elastic_manager = manager.Manager()
def create_mesh(
elastic_manager: manager.Manager, devices_per_slice: int
) -> jax.sharding.Mesh:
device_array = np.asarray(elastic_manager.active_devices).reshape(
(elastic_manager.active_slice_count, devices_per_slice)
)
return jax.sharding.Mesh(device_array, axis_names=("replica", "model"))
Quando o número de frações ativas mudar no modo de redimensionamento de réplica, use um carregador de dados elástico, como grain.experimental.ElasticIterator. Os iteradores de dados padrão por host salvam o estado por host e não podem ser restaurados em um número diferente de hosts, enquanto ElasticIterator rastreia uma única posição de conjunto de dados global que pode ser restaurada em qualquer número de frações ativas.
Loop de treinamento elástico
Um loop de treinamento elástico coordena o monitoramento de frações, a recuperação de falhas e os snapshots na memória, como mostrado no exemplo de código a seguir:
- A função
traincria uma única instânciamanager.Manager()e umSnapshotterdo Orbax fora do wrapper de nova tentativa para que o rastreamento de frações e os snapshots de memória do host persistam entre as tentativas. - Decorar a função
run_training_loopinterna com@elastic_manager.elastic_retryaguarda frações íntegras, definejax.default_devicecomo uma fração ativa, mantém a linha de execução do monitor em segundo plano ativa em todas as etapas e repete o loop quando ocorre umjax.errors.JaxRuntimeError. - Ao se recuperar de uma falha de fração no início de uma tentativa, seu código
precisa recriar a malha do dispositivo de
elastic_manager.active_devicese chamarsnapshotter.loadpara restaurar o snapshot mais recente nas fragmentações de malha atualizadas, permitindo que os buffers do dispositivo da tentativa com falha saiam do escopo. - Durante cada etapa do loop, o código chama periodicamente
snapshotter.savepara fazer backup do estado de treinamento e verificaelastic_manager.available_inactive_slicespara detectar fatias de substituição íntegras. Quando as frações de substituição estão disponíveis, o código as adiciona aelastic_manager.active_slice_indices, recria a malha, reinicializa o loop de treinamento e coloca o estado na malha expandida.
O exemplo a seguir mostra como implementar um loop de treinamento elástico
combinando manager.Manager, elastic_retry e o Snapshotter
na memória do Orbax:
import jax
import orbax.checkpoint as ocp
import pathwaysutils
from pathwaysutils.elastic import manager
pathwaysutils.initialize()
def train(
final_step: int,
snapshot_interval: int, # in steps
devices_per_slice: int,
):
elastic_manager = manager.Manager()
snapshotter = ocp.v1.training.pathways.Snapshotter(replica_axis_index=0)
@elastic_manager.elastic_retry(
minimum_slice_count=1,
poll_interval=10, # in seconds
timeout=300, # in seconds
retry_policy=manager.ElasticRetryLimit(max_attempts=10),
)
def run_training_loop():
mesh = create_mesh(elastic_manager, devices_per_slice)
state, abstract_state, jitted_train_step, data_iterator = (
initialize_training_loop(mesh)
)
step = 0
# Restore from the latest in-memory snapshot after a slice failure.
if snapshotter.latest is not None:
snapshotter.join()
restored = snapshotter.load({
"state": abstract_state,
"step": step,
"iterator_state": data_iterator.get_state(),
})
state = restored["state"]
step = int(restored["step"]) + 1
data_iterator.set_state(restored["iterator_state"])
while step < final_step:
batch = next(data_iterator)
state = jitted_train_step(state, batch)
if step % snapshot_interval == 0:
snapshotter.save(
step,
{
"state": state,
"step": step,
"iterator_state": data_iterator.get_state(),
},
)
# If replacement slices are healthy, reinitialize and place state onto the new mesh.
if elastic_manager.available_inactive_slices:
elastic_manager.active_slice_indices |= (
elastic_manager.available_inactive_slices
)
mesh = create_mesh(elastic_manager, devices_per_slice)
iterator_state = data_iterator.get_state()
_, abstract_state, jitted_train_step, data_iterator = (
initialize_training_loop(mesh)
)
data_iterator.set_state(iterator_state)
state_shardings = jax.tree.map(lambda x: x.sharding, abstract_state)
state = jax.device_put(state, state_shardings)
step += 1
return state
state = run_training_loop()
snapshotter.join()
return state
Configurar o gerenciador elástico e o comportamento de nova tentativa
Você instancia manager.Manager(devices=None) uma vez por processo. Se você omitir
devices, o gerenciador vai usar jax.devices() e agrupar os dispositivos pelo
atributo slice_index.
A instância Manager expõe as seguintes propriedades para inspecionar a topologia do cluster durante o treinamento:
slice_to_devices: um mapeamento de cadaslice_indexpara a sequência de objetosjax.Deviceque pertencem a essa fração.all_slice_indices: umafrozensetde todos os índices de fração na alocação.active_slice_indices: umfrozensetde índices de fração que participam da malha de computação ativa.active_devices: uma lista de objetosjax.Deviceque pertencem às frações ativas, ordenadas porslice_index.inactive_slice_indices: umfrozensetde índices de fração na alocação que não estão participando da malha de computação.available_inactive_slices: umfrozensetde índices de fração inativos que a linha de execução do monitor em segundo plano detectou como íntegros e prontos para voltar.total_slice_counteactive_slice_count: o número total de frações alocadas e o número de frações ativas.default_device: dispositivo0de uma fração ativa, que oelastic_retrydefine automaticamente como o dispositivo JAX padrão para cada tentativa.
É possível personalizar o comportamento de nova tentativa e escalonamento transmitindo os seguintes argumentos
para @elastic_manager.elastic_retry:
minimum_slice_count: o número mínimo de frações íntegras necessárias antes de iniciar a execução inicial ou uma nova tentativa. Se definido comoNone(o padrão),elastic_retryaguarda todas as frações na alocação (total_slice_count), operando no modo de pausa e retomada. Se definido como um número inteiro menor (como1),elastic_retryopera no modo de redimensionamento de réplica.poll_interval: o intervalo em segundos entre as verificações de integridade de fração realizadas pelo gerenciador de treinamento elástico (Manager) durante o treinamento. O padrão é10.timeout: o número máximo de segundos para aguardar até que as frações deminimum_slice_countfiquem ativas antes de gerar umTimeoutError. O padrão éNone(sem tempo limite).pre_callback: um callback opcional que é executado no início de cada tentativa após a atualização deactive_slice_indicese antes da execução da função de loop de treinamento encapsulada.on_elastic_event_callback: um callback opcional que é executado depois que uma redução de fraçãojax.errors.JaxRuntimeErroré detectada, antes do início da próxima tentativa de repetição. Use esse callback para limpar checkpoints incompletos ou redefinir iteradores de dados.retry_policy: um(attempt: int, error: Exception) -> boolque pode ser chamado e determina se as novas tentativas devem continuar após um evento elástico. Usemanager.ElasticRetryLimit(max_attempts)para limitar o número total de tentativas. Se omitido,elastic_retrytenta novamente indefinidamente.
Em um evento elástico, o JAX libera os buffers do dispositivo da tentativa com falha quando o frame de pilha é desfeito e nenhuma variável faz referência a essas matrizes. Evite manter referências a matrizes de dispositivos de uma tentativa com falha dentro de variáveis globais ou callbacks de longa duração para que a memória seja liberada antes do início da próxima tentativa.
Snapshots na memória com o Orbax
A classe ocp.v1.training.pathways.Snapshotter (disponível na
versão 0.12.6 e mais recentes do orbax-checkpoint) mantém um backup na memória do
estado de treinamento na memória pinned_host em todas as frações:
Snapshotter(replica_axis_index=0): inicializa o snapshotter e especifica qual eixo de malha representa as réplicas de fração paralelas de dados. Todas as matrizes JAX no estado salvo precisam ser replicadas ao longo desse eixo de malha para que as frações sobreviventes mantenham uma cópia completa do modelo e do estado do otimizador quando uma fração falhar. Se o estado tiver matrizes de dispositivo único, replique-as na malha antes de chamarsave.save(step, state): transfere de forma assíncronastatepara a memóriapinned_hoste o divide em árvores por réplica ao longo dereplica_axis_index. Se uma transferência de snapshot anterior ainda estiver em andamento,savevai pular a nova solicitação para evitar que o loop de treinamento seja interrompido.load(abstract_state, reset_snapshot_state=True): sonda as réplicas de host salvas para identificar fatias sobreviventes, concatena as réplicas íntegras e refragmenta o estado nas fragmentações de destino especificadas porabstract_state.join(): bloqueia até que qualquer operação de snapshot em fila seja concluída.latest: retorna metadados (incluindostep) para o snapshot concluído mais recentemente ouNonese nenhum snapshot tiver sido concluído.
Reduzir a fragmentação em caso de falha de fração
Quando uma fração de TPU falha ou é interrompida durante a execução, o Pathways gera um
jax.errors.JaxRuntimeError (como DATA_LOSS, DEADLINE_EXCEEDED,
NOT_FOUND ou INTERNAL). O decorador elastic_retry captura essa
exceção e verifica elastic.is_error_due_to_slice_down(error):
- Se o erro não for causado por um evento de redução,
elastic_retryvai gerar novamente a exceção imediatamente. - Se o erro for causado por um evento de redução de fração,
elastic_retryinterrompe qualquer trace ativo do criador de perfil do JAX, executaon_elastic_event_callbacke avaliaretry_policy(attempt, error). - Antes de iniciar a próxima tentativa,
elastic_retrychamaelastic.wait_for_slicesuma vez, que pesquisa a integridade da fração até que pelo menosminimum_slice_countfrações estejam ativas, atualizaelastic_manager.active_slice_indicese executa a função de loop de treinamento nas frações sobreviventes.
Aumentar a fragmentação quando as frações se juntarem novamente
Quando a carga de trabalho é executada em um número reduzido de frações no modo de redimensionamento de réplica,
a linha de execução do monitor em segundo plano executa periodicamente verificações de integridade em
elastic_manager.inactive_slice_indices. Quando as frações reparadas ou reprogramadas
passam na verificação de integridade, a linha de execução do monitor adiciona os índices de fração a
elastic_manager.available_inactive_slices.
Quando elastic_manager.available_inactive_slices não está vazio no final de uma
etapa de treinamento, a carga de trabalho é escalonada verticalmente sem perder o progresso
do treinamento:
- Adicionar os índices de fração em
elastic_manager.available_inactive_slicesaelastic_manager.active_slice_indicesincorpora as frações reunidas emelastic_manager.active_devicese as limpa deavailable_inactive_slices. - A recriação da malha do dispositivo de
elastic_manager.active_devicese a reinicialização da etapa de treinamento e do iterador de dados adaptam o cálculo à topologia expandida. - Chamar
jax.device_putcoloca o estado de treinamento atual nas novas fragmentações de malha sem restaurar de um snapshot.
Hot-swap
A troca rápida se refere a um recurso da API JobSet do GKE em que um job de maior prioridade pode assumir rapidamente os recursos de um job de menor prioridade, minimizando o tempo de inatividade e garantindo uma recuperação mais rápida.
Quando um JobSet é criado, o GKE programa a carga de trabalho em várias frações, conforme especificado na configuração do JobSet. Se ocorrer uma falha de hardware em uma ou mais frações, os pods afetados serão marcados como com falha. Se o cluster do GKE tiver uma fração extra executando um job de prioridade mais baixa, o sistema do JobSet vai remapear a carga de trabalho da fração com falha do job de prioridade mais alta para essa fração extra. Esse remapeamento geralmente leva menos de um minuto.
Quando um JobSet é reiniciado, a troca automática pode ocorrer nas seguintes situações:
- Modo padrão: se houver frações de TPU ociosas e de reserva disponíveis no mesmo cluster, o programador do Kubernetes prioriza a programação dos jobs reiniciados nessas frações, em vez de esperar que as frações com falha sejam reparadas. Esse comportamento de programação oferece uma recuperação mais rápida.
- Cargas de trabalho heterogêneas: em clusters que executam várias cargas de trabalho com um
recurso
PriorityClassdo Kubernetes configurado, um JobSet reiniciado pode acionar uma troca a quente. Se a afinidade do job reiniciado corresponder aos recursos de um job de prioridade mais baixa, o Kubernetes vai interromper o job de prioridade mais baixa para que o job de prioridade mais alta possa ser iniciado sem esperar pelo reparo da fração. Por exemplo, é possível configurar os pods de worker do Pathways com prioridades diferentes usando um recursoPriorityClass.
Para usar prioridades no cluster, defina um manifesto PriorityClass:
kind: PriorityClass
metadata:
name: high-prior-job
value: 2000
globalDefault: false
description: "This priority class should be used for high priority job."
Aplique esse YAML ao cluster do GKE:
kubectl apply -f high-prior-job.yaml
Em seguida, anexe a nova classe de prioridade ao job de worker do Pathways adicionando o
seguinte texto à podspec do pod pathways-worker.
priorityClassName: high-prior-job