Chad Schuster, Principal na prática de gestão de riscos financeiros da Milliman, apresentou uma experiência prática de construção de modelos computacionais de alto desempenho usando Python e Numba, em vez de depender integralmente de aplicações escritas em C++. A experiência parte de um problema comum nos serviços financeiros: modelos que simulam fluxos de caixa em um grande número de cenários exigem muita capacidade computacional, enquanto as equipes desejam manter a velocidade de desenvolvimento e a facilidade de manutenção do Python.
O problema é especialmente relevante em modelos de seguros de vida e produtos de aposentadoria, como annuities, nos quais os cálculos são usados para avaliar obrigações futuras e executar vários cenários. Segundo a apresentação, as seguradoras historicamente dependiam de redes locais com entre 5.000 e 10.000 nós, mas a migração para a nuvem tornou o tempo de execução e o custo de cada operação mais evidentes nas decisões de infraestrutura.
O que a Numba acrescenta ao Python?
O Python tradicional, ou CPython, interpreta o código durante a execução, enquanto linguagens compiladas como C++ transformam o código em instruções de máquina antes da execução. A Numba tenta reduzir essa diferença por meio da compilação Just-in-Time, pois compila as funções elegíveis durante a execução do programa usando o LLVM e, em seguida, substitui a função original pela implementação compilada.
O desenvolvedor normalmente ativa esse processo por meio de decoradores do Python, como JIT ou njit. A Numba opera no nível da função: examina o código e o transforma em uma representação intermediária própria; depois infere os tipos das variáveis, dos parâmetros e dos valores de retorno, antes de reduzir a representação para LLVM IR e realizar as otimizações necessárias. Se a função for chamada com tipos diferentes, a Numba pode criar implementações compiladas especializadas para cada conjunto de tipos, por meio do que chama de polymorphic dispatch.
Os ganhos de desempenho não são um número fixo
Na prova de conceito apresentada por Schuster, a Numba tornou o programa aproximadamente 75 vezes mais rápido em comparação com o Python interpretado. Em outro modelo, o resultado foi diferente: transferir os cálculos numéricos intensivos para a Numba na CPU produziu uma aceleração de aproximadamente duas vezes; depois, a migração para a GPU gerou uma aceleração adicional de 750 vezes naquela execução específica. A apresentação afirmou que esse nível de melhoria fez com que uma unidade de GPU equivalesse aproximadamente a 750 núcleos usados naquela execução, com uma redução estimada do custo para cerca de um décimo.
No entanto, esses resultados não representam uma promessa geral para todas as aplicações. A melhoria depende da quantidade de código compilável, do volume das operações computacionais em comparação com as operações de entrada e saída, da eficiência do Python original e da capacidade do LLVM de otimizar o código resultante. Além disso, o processamento em GPU não é adequado para todos os algoritmos; por isso, o gargalo real deve ser medido antes de o modelo ser redesenhado.
O que muda na prática para as equipes de engenharia?
A experiência recomenda manter em Python as camadas de preparação de dados, entradas e saídas, transferindo para a Numba apenas a parte numericamente intensiva sempre que possível. Essa abordagem reduz o escopo da reescrita e preserva grande parte do ambiente Python, enquanto concentra o esforço de otimização nas funções que consomem a maior parte do tempo de execução. Quando as funções numéricas são limitadas e claras, Schuster considera que experimentar a Numba pode ser uma opção direta para quem deseja permanecer no Python.
Já quando uma lógica ampla e complexa é introduzida na Numba, aumentam os custos de projeto e manutenção. A experiência recorreu a arrays do NumPy, tuples e estruturas de dados simples, porque o suporte da Numba não abrange todos os recursos do Python. Entre as limitações mencionadas estão dicionários com tipos flexíveis, exceções, gerenciadores de contexto, closures e listas criadas por comprehensions, além do suporte limitado a algumas funções comuns, como print, sorted e getattr.
Limitações que devem ser consideradas antes da adoção
A programação orientada a objetos continua sendo um ponto fraco importante nessa experiência. A Numba oferece recursos experimentais, como jitclasses e structrefs, para proporcionar um comportamento semelhante ao de objetos, mas eles podem mudar entre versões e não oferecem suporte a GPU, o que os tornou inadequados para a abordagem adotada pela equipe. Por isso, a aplicação adotou um estilo mais próximo da programação funcional e estruturas de dados simples, embora o design orientado a objetos fosse desejável por razões relacionadas à manutenção e à entrega dos modelos aos clientes.
Além disso, erros de inferência de tipos e erros de lowering podem ser difíceis de rastrear, especialmente em programas com muitas camadas de chamadas. O erro pode apontar para uma função distante do local em que o problema realmente surgiu. Uma abordagem prática sugerida é usar JIT em vez de njit quando necessário, permitindo desativar temporariamente a Numba e retornar ao Python interpretado para facilitar a depuração, reativando depois a aceleração na execução de produção.
Também existe um tempo inicial de compilação que ocorre na primeira chamada de cada função e pode aumentar quando a técnica de inlining é usada em larga escala. É possível recorrer à compilação Ahead-of-Time para evitar esse atraso, mas isso pode reduzir a capacidade do código de se adaptar ao dispositivo efetivamente utilizado, em comparação com a compilação Just-in-Time.
Leitura editorial: o principal valor dessa experiência não está no número de 750 vezes em si, mas na abordagem que relaciona a medição de desempenho à divisão do modelo. A Numba é adequada quando o gargalo é computacional e pode ser isolado em funções com tipos e estruturas claros; já transformar todo um sistema Python em código compilável pode transferir o problema da lentidão de execução para a complexidade do desenvolvimento e da depuração. Portanto, as equipes financeiras e de engenharia devem equilibrar velocidade de execução, facilidade de manutenção, tempo de compilação e compatibilidade com GPU antes de considerá-la uma alternativa abrangente ao C++ ou a uma reformulação mais ampla do sistema.