Infraestrutura de Testes
A infraestrutura de testes do PyTorch é frequentemente gerada dinamicamente em diferentes dispositivos e tipos de dados, o que explica por que os nomes dos testes no sistema de integração contínua (CI) podem parecer diferentes dos nomes das classes e métodos no arquivo de origem. Este artigo explica como os testes genéricos de dispositivos, OpInfos, a função instantiate_device_type_tests() e a divisão de testes em pedaços (sharding) se encaixam, e como os contribuintes podem executar e depurar os testes do PyTorch de forma mais eficaz.
Se você já abriu uma solicitação de pull request no PyTorch, assistiu a um teste gerado, como TestLinalgCUDA.test_matmul_cuda_float32, falhar no CI e se perguntou de onde veio esse nome – ou tentou executar um teste pelo nome da origem e obteve a mensagem “nenhum teste coletado” – este guia é para você. A infraestrutura de testes do PyTorch é projetada para escala. Dependendo dos decoradores utilizados e dos metadados do operador fornecidos por meio de OpInfos, um único método de teste pode se expandir por vários dispositivos, tipos de dados e operadores automaticamente. É isso que permite que o PyTorch valide milhares de combinações sem precisar de milhares de testes escritos à mão. No entanto, também significa que o teste que você escreve no arquivo de origem não é sempre o exato teste que o CI executa, o que pode ser confuso na primeira vez que você o encontra.
Observação: Muitos ajudantes discutidos neste guia estão localizados em torch.testing._internal, que é a infraestrutura de testes interna do PyTorch. Se você estiver testando o seu próprio projeto, use APIs públicas como pytest e torch.testing.assert_close em vez disso. Um dos primeiros momentos confusos para novos contribuintes do PyTorch é tentar executar um teste pelo nome da classe e do método que eles veem no arquivo de origem: muitas vezes, isso pode retornar “nenhum teste coletado”. Isso geralmente não ocorre porque o teste está faltando, mas porque a classe no arquivo de origem é um modelo, e não a classe final que o executor de testes vê.
Quando o arquivo é importado, a função instantiate_device_type_tests() expande o modelo em classes concretas específicas de dispositivo, como TestTorchCPU, TestTorchCUDA ou TestTorchMPS. Se o teste também for parametrizado por tipo de dados, o nome do método gerado pode incluir o dispositivo e o tipo de dados, por exemplo, test_matmul_cuda_float32. Essas classes geradas são construídas a partir da classe de modelo original e das bases de teste específicas de dispositivo do PyTorch, então elas ainda herdam o comportamento compartilhado fornecido pela classe de teste interna do PyTorch.
Para depuração local, geralmente é mais fácil filtrar pelo padrão de nome do teste gerado em vez de visar a classe de modelo original diretamente: uma vez que você sabe que o PyTorch gera os nomes dos testes executáveis durante a importação, as falhas do CI se tornam muito mais fáceis de mapear para o teste de origem. O PyTorch é executado em dispositivos como CPU, CUDA, MPS e XPU, e muitos testes precisam validar o comportamento em diferentes tipos de dados, como float16, float32, float64, bfloat16, integer e outros. Escrever um teste separado para cada combinação de dispositivo e tipo de dados rapidamente se tornaria um pesadelo de manutenção.
Por isso, o PyTorch usa modelos de teste. Você escreve um método de teste com parâmetros de dispositivo e tipo de dados: quando o Python importa o arquivo de teste, a função instantiate_device_type_tests() expande esse modelo nos dispositivos e tipos de dados selecionados. Por exemplo, uma classe de modelo pode produzir classes geradas como TestMatmulCPU, TestMatmulCUDA e TestMatmulMPS, com métodos gerados como test_basic_cuda_float32.
Os nomes gerados seguem um padrão específico: então, um modelo como TestMatmul.test_basic pode se tornar TestMatmulCUDA.test_basic_cuda_float32. O dispositivo aparece em letras maiúsculas no nome da classe e em letras minúsculas no nome do método. É por isso que as falhas do CI mostram nomes gerados em vez de apenas o nome do modelo que você escreveu. O nome gerado informa exatamente qual dispositivo e tipo de dados falharam.
A infraestrutura de testes do PyTorch é mais fácil de entender como um conjunto de camadas conectadas. Os contribuintes normalmente interagem com as camadas do meio: instância de dispositivo, decoradores de parametrização, OpInfos e utilitários de teste. A orquestração do CI está acima delas, enquanto as utilidades básicas fornecem a fundação compartilhada. OpInfos são entradas de metadados que descrevem como um operador do PyTorch deve ser testado. Em vez de escrever um teste separado para cada operador, o PyTorch usa modelos de teste genéricos que leem metadados OpInfo e executam as mesmas verificações em muitos operadores.
Um OpInfo pode definir coisas como o nome do operador, variantes, tipos de dados suportados, entradas de amostra, skips esperados, decoradores e regras de tolerância. Testes genéricos em arquivos como test_ops.py consomem op_db por meio de @ops(…), que passa o operador selecionado, o dispositivo e o tipo de dados para o teste. É assim que uma entrada de operador pode participar de muitos tipos de cobertura: correção para a frente, comportamento de tipo de dados e dispositivo, verificações de gradiente, caminhos relacionados à compilação e validação de estilo Meta/FakeTensor – dependendo do teste e dos metadados do operador.
Então, quando você vê um teste gerado, como TestCommonCUDA.test_variant_consistency_eager_torch_matmul_cuda_float32, geralmente significa que um teste genérico baseado em OpInfo está sendo executado contra a OpInfo do