Back-ends do acelerador

É bastante simples descrever um cálculo Tensor , mas quando e como esse cálculo é executado dependerá de qual back-end é usado para os Tensor e quando os resultados são necessários na CPU do host.

Nos bastidores, as operações em Tensor s são enviadas para aceleradores como GPUs ou TPUs ou executadas na CPU quando nenhum acelerador está disponível. Isso acontece automaticamente para você e facilita a execução de cálculos paralelos complexos usando uma interface de alto nível. Entretanto, pode ser útil entender como esse despacho ocorre e poder personalizá-lo para obter desempenho ideal.

Swift para TensorFlow tem dois back-ends para realizar computação acelerada: modo ansioso TensorFlow e X10. O back-end padrão é o modo ansioso do TensorFlow, mas isso pode ser substituído. Está disponível um tutorial interativo que orienta você no uso desses diferentes back-ends.

Modo ansioso do TensorFlow

O back-end do modo ansioso do TensorFlow aproveita a API TensorFlow C para enviar cada operação Tensor para uma GPU ou CPU conforme ela é encontrada. O resultado dessa operação é então recuperado e passado para a próxima operação.

Esse despacho operação por operação é simples de entender e não requer configuração explícita em seu código. No entanto, em muitos casos, isso não resulta em desempenho ideal devido à sobrecarga resultante do envio de muitas operações pequenas, combinada com a falta de fusão e otimização de operações que pode ocorrer quando gráficos de operações estão presentes. Por fim, o modo ansioso do TensorFlow é incompatível com TPUs e só pode ser usado com CPUs e GPUs.

X10 (rastreamento baseado em XLA)

X10 é o nome do back-end Swift para TensorFlow que usa rastreamento de tensor lento e o compilador de otimização XLA para, em muitos casos, melhorar significativamente o desempenho em relação ao despacho operação por operação. Além disso, adiciona compatibilidade para TPUs , aceleradores especificamente otimizados para os tipos de cálculos encontrados em modelos de aprendizado de máquina.

O uso do X10 para cálculos Tensor não é o padrão, então você precisa ativar esse back-end. Isso é feito especificando que um Tensor é colocado em um dispositivo XLA:

let tensor1 = Tensor<Float>([0.0, 1.0, 2.0], on: Device.defaultXLA)
let tensor2 = Tensor<Float>([1.5, 2.5, 3.5], on: Device.defaultXLA)

Depois desse ponto, descrever um cálculo é exatamente o mesmo que para o modo ansioso do TensorFlow:

let tensor3 = tensor1 + tensor2

Maiores detalhes podem ser fornecidos ao criar um Tensor , como que tipo de acelerador usar e até mesmo qual, se vários estiverem disponíveis. Por exemplo, um Tensor pode ser criado no segundo dispositivo TPU (assumindo que seja visível para o host em que o programa está sendo executado) usando o seguinte:

let tpuTensor = Tensor<Float>([0.0, 1.0, 2.0], on: Device(kind: .TPU, ordinal: 1, backend: .XLA))

Nenhum movimento implícito de Tensor s entre dispositivos é executado, portanto, se dois Tensor s em dispositivos diferentes forem usados ​​juntos em uma operação, ocorrerá um erro de tempo de execução. Para copiar manualmente o conteúdo de um Tensor para um novo dispositivo, você pode usar o inicializador Tensor(copying:to:) . Algumas estruturas de maior escala que contêm Tensor s dentro delas, como modelos e otimizadores, têm funções auxiliares para mover todos os seus Tensor s internos para um novo dispositivo em uma única etapa.

Ao contrário do modo ansioso do TensorFlow, as operações que usam o back-end X10 não são despachadas individualmente à medida que são encontradas. Em vez disso, o despacho para um acelerador só é acionado pela leitura dos valores calculados de volta ao host ou pela colocação de uma barreira explícita. A forma como isso funciona é que o tempo de execução começa a partir do valor que está sendo lido para o host (ou o último cálculo antes de uma barreira manual) e traça o gráfico dos cálculos que resultam naquele valor.

Este gráfico rastreado é então convertido para a representação intermediária XLA HLO e passado para o compilador XLA para ser otimizado e compilado para execução no acelerador. A partir daí, todo o cálculo é enviado para o acelerador e o resultado final é obtido.

O cálculo é um processo demorado, portanto o X10 é melhor usado com cálculos massivamente paralelos que são expressos por meio de um gráfico e executados muitas vezes. Valores hash e cache são usados ​​para que gráficos idênticos sejam compilados apenas uma vez para cada configuração exclusiva.

Para modelos de aprendizado de máquina, o processo de treinamento geralmente envolve um loop onde o modelo é submetido à mesma série de cálculos continuamente. Você desejará que cada uma dessas passagens seja vista como uma repetição do mesmo traço, em vez de um gráfico longo com unidades repetidas dentro dele. Isso é habilitado pela inserção manual de uma chamada para a função LazyTensorBarrier() nos locais do seu código onde você deseja que o rastreamento termine.

Suporte de precisão mista no X10

O treinamento com precisão mista via X10 é suportado e APIs de baixo e alto nível são fornecidas para controlá-lo. A API de baixo nível oferece duas propriedades computadas: toReducedPrecision e toFullPrecision que convertem entre precisão total e reduzida, junto com isReducedPrecision para consultar a precisão. Além dos Tensor s, modelos e otimizadores podem ser convertidos entre precisão total e reduzida usando esta API.

Observe que a conversão para precisão reduzida não altera o tipo lógico de um Tensor . Se t for um Tensor<Float> , t.toReducedPrecision também será um Tensor<Float> com uma representação subjacente de precisão reduzida.

Tal como acontece com os dispositivos, não são permitidas operações entre tensores de diferentes precisões. Isso evita promoção silenciosa e indesejada para floats de 32 bits, que seriam difíceis de detectar pelo usuário.