Quando a GPU Compensa: O Roofline do Sweep de Parâmetros, Onde um 167x de Manchete É na Verdade 27x de Algoritmo Vezes 6.2x de Hardware
Parte da série "Backtests sem ilusões".
A escada de velocidade terminou em uma nota deliberadamente insatisfatória. Tínhamos levado um sweep de parâmetros de 80 combos de 69.9 segundos de pandas para 0.23 segundos de numba paralelo em uma CPU de laptop — um 298x medido — e então argumentamos que uma GPU não era o degrau que faltava. A seção de comentários não aceitou isso em silêncio, e fez bem. "Não é o degrau que falta" é uma afirmação sobre uma carga de trabalho, em um tamanho. Não é uma lei da natureza. Então este artigo faz a coisa honesta e coloca a GPU na bancada de testes.
O resultado não é um veredito, é uma curva. A vantagem da GPU sobre a CPU não é um número único que se possa imprimir em um slide; é uma função de quanto trabalho você entrega a ela por chamada. Na nossa pré-computação de indicadores multi-timeframe, o speedup da GPU sobre o motor de CPU vai de 54.5x quando lhe pedimos uma combinação de parâmetros por vez até 359.6x quando pedimos 61 de uma só vez. Mesmo kernel, mesmos dados, mesmo hardware — a única coisa que mudou foi o batch. Um benchmark que reporta um desses números e esconde o outro não está medindo a GPU, está escolhendo uma manchete.
E mesmo 359.6x não é o que parece. Desmonte o número e uma grande manchete multi-timeframe de 167x se decompõe em 27x de algoritmo — uma reescrita que também torna a CPU 27x mais rápida — vezes 6.2x de hardware de verdade. A GPU não fez o 27x. A matemática fez. Este artigo é sobre distinguir os dois, porque confundi-los é como uma placa de vídeo de $2,000 acaba sendo vendida para resolver um problema que um git commit teria resolvido de graça.
Procedência: todos os números abaixo foram medidos em um Apple M2 Max, fp32 na GPU Metal via MLX contra numba em fp64 em doze threads de CPU, a partir dos nossos scripts de benchmark engine_multitf_gpu.py e bench_param_sweep.py, cada um protegido por uma verificação de equivalência que confirma que os caminhos de GPU e CPU produzem os mesmos trades. Sem paper de acompanhamento público desta vez — os scripts são o registro.
A pergunta é um roofline, não um benchmark

A razão pela qual "quanto mais rápida é a GPU" não tem uma resposta única é o roofline model (Williams, Waterman & Patterson, 2009). O desempenho alcançável de um processador é limitado por dois tetos ao mesmo tempo: um plano, definido pelo pico de computação (FLOP/s), e um inclinado, definido pela largura de banda de memória vezes a intensidade aritmética — o número de operações úteis que você faz por byte movido. Trabalho barato por byte vive sob o teto inclinado e é limitado por largura de banda; trabalho rico por byte alcança o teto plano e é limitado por computação. O teto plano da GPU se ergue muito acima do da CPU, mas o teto inclinado dela e, crucialmente, o seu custo fixo por chamada não escalam para baixo com a mesma elegância.
Para um sweep de parâmetros, um terceiro termo domina o lado esquerdo do gráfico: o overhead de lançamento e transferência. Toda chamada à GPU paga um preço aproximadamente fixo — despachar o kernel, mover as entradas através da fronteira de memória (unificada no Apple Silicon, mas não gratuita), trazer os resultados de volta. Modele o wall time de um batch de combinações como
onde é esse overhead fixo, é o custo marginal por combo da GPU e é o da CPU. O speedup é então
Essa única fração explica o artigo inteiro. Com pequeno, o no denominador esmaga a razão — você pagou para acordar a GPU e mal a usou. À medida que cresce, você amortiza sobre mais combos e o speedup sobe rumo à sua assíntota , a razão de hardware verdadeira. O ponto de meio caminho fica em : um "ponto de crista" no espaço de batch que diz quão largo o seu sweep precisa ser antes que a GPU seja sequer metade do que pode ser. Um sweep de algumas dezenas de combos fica bem à esquerda dessa crista. Um sweep de milhares fica na parte plana, onde a GPU finalmente faz jus ao seu teto.
Então a pergunta certa nunca é "a GPU é mais rápida". É "meu sweep está à direita da crista, e meu trabalho por combo é limitado por computação o suficiente para alcançar o teto plano quando chegar lá". As duas coisas precisam ser verdadeiras. O resto deste artigo mede onde esses limiares realmente caem.
O veredito single-timeframe: a GPU ganha por pouco

Comece pela carga de trabalho que a escada de velocidade usou: um sweep HMA/HMA3 single-timeframe, 80 combinações sobre 150,000 barras. Adicionamos um sexto degrau àquela escada — M5, indicadores na GPU da Apple via MLX, com os trades ainda extraídos na CPU. Aquecido, melhor de três, o gate de equivalência verde:
| Método | Wall time | vs pandas | Combos/s |
|---|---|---|---|
| M0 pandas + loop | 287.08s | 1.0x | 0.3 |
| M2 numba (1 núcleo) | 2.02s | 142x | 39.7 |
| M4 mp + numba (12 núcleos) | 0.33s | 883x | 245.9 |
| M5 MLX GPU (fp32) | 0.10s | 2796x | 779.2 |
Contra a baseline serial naive, a GPU parece heroica — 2,796x. Mas essa é uma comparação que ninguém honesto deveria fazer: ela coloca uma boa implementação de GPU contra a pior implementação de CPU. Alinhe a GPU contra a CPU que você de fato colocaria em produção — o mesmo kernel em todos os doze núcleos, M4 — e a vitória desaba para sóbrios 3.2x (779 contra 246 combos por segundo). Uma placa de vídeo inteira, rodando o sweep todo, vence um pool de CPU de doze núcleos por um fator de três.
Três vírgula dois não é nada desprezível. Também não é o motivo pelo qual alguém compra uma GPU. E é exatamente o que o roofline prevê para um sweep tão estreito: 80 combos está à esquerda da crista. O overhead fixo de lançamento e transferência ainda é uma fatia relevante de um job de 0.10 segundo, então nunca chegamos à assíntota . Pior: parte do custo por combo é a passada O(n) de extração de trades, que deliberadamente deixamos na CPU — um termo que a GPU não consegue acelerar de forma alguma (mais sobre o porquê na próxima seção). Para um loop de pesquisa single-timeframe deste tamanho, o veredito original da escada de velocidade se mantém: a GPU não é o degrau que falta. O numba paralelo já colocava você em 0.23–0.33 segundos, e raspar isso para 0.10 não é o que desbloqueia um pesquisador. A orquestração em volta do sweep é.
A expressão interessante nesse veredito é deste tamanho. Mova-se para a direita ao longo do eixo de batch e a história muda.
Onde o custo realmente mora
Antes de aumentar o batch, olhe para o que estamos de fato pagando, porque o roofline só recompensa você se a parte cara for limitada por computação. Faça o profiling do sweep e quase tudo é uma coisa só: convoluções de média móvel ponderada. Uma HMA são três WMAs; uma HMA3 são quatro; cada combinação as reexecuta sobre a série inteira. A extração de trades — percorrer os dois arrays de indicadores, encontrar as trocas de sinal de hma - hma3, registrar os fills — é uma única passada barata em O(n). O sweep é uma carga de trabalho de convolução vestindo fantasia de estratégia de trading.
Essa divisão corresponde exatamente aos dois regimes do roofline:
- As convoluções são limitadas por computação. Cada preço é lido em muitas somas de janelas sobrepostas, então a intensidade aritmética — operações por byte movido — é alta. Esse trabalho estica a mão para o teto plano de computação, e o teto plano da GPU é o que se ergue mais alto. Melhor ainda, as janelas se sobrepõem entre combinações: uma WMA de comprimento 40 serve a dezenas de combos, então uma pré-computação em batch a compartilha uma vez em vez de recomputá-la por combo. Fazer batch não apenas amortiza o overhead de lançamento; eleva a intensidade aritmética ao reutilizar leituras. Esta é a parte que pertence à GPU.
- A extração de trades é limitada por largura de banda e cheia de branches. Uma passada sequencial, branches dependentes dos dados a cada cruzamento, essencialmente nenhuma reutilização. Sua intensidade aritmética está perto do piso, e seu fluxo de controle é hostil a um dispositivo SIMD. Empurrá-la para a GPU compraria pouco e custaria muito; ela fica na CPU. O que significa que ela é a cauda serial de uma divisão regida pela lei de Amdahl — um piso fixo que o speedup da GPU nunca consegue perfurar, e parte do motivo pelo qual o número single-TF saturou em 3.2x.
Há uma segunda lição, mais afiada, escondida na versão multi-timeframe deste kernel, e ela é a fonte do 27x que seguimos prometendo explicar. O motor multi-TF alinha uma HMA de timeframe superior sobre o índice base de 1 minuto sem look-ahead. Escrito do jeito óbvio, isso é trabalho O(length^1.5) por barra — recomputar as médias móveis do timeframe superior a cada barra base. Mas a HMA alinhada é linear em um buffer curto dos últimos candles fechados do timeframe superior mais o close corrente, então toda a computação por barra colapsa em um vetor fixo de pesos: um conv1d sobre a série de candles fechados seguido de um gather em O(n). Centenas de milhões de operações redundantes viram uma convolução sobre uma série muito mais curta.
Esse colapso é uma vitória algorítmica, não de hardware. É uma fórmula melhor. Ela roda na GPU, e roda igualmente bem na CPU — np.correlate mais um gather, em fp64. Guarde isso com firmeza: o maior fator individual da manchete multi-TF é uma reescrita disponível para uma máquina sem GPU nenhuma. Quando finalmente decompusermos o 167x, este é o 27x.
A vantagem cresce com o tamanho do batch

Agora a medição que o roofline nos mandou fazer. Pegue o eixo caro — a pré-computação da HMA alinhada de timeframe superior sobre a série base de 1 minuto, o stream de candles mais longo que temos — e alimente a GPU com um número crescente de combinações de comprimento por chamada, . A baseline de CPU aqui é o motor honesto de produção: numba com prange em todos os doze núcleos. Para cada batch, medimos os dois e tomamos a razão.
| Batch (combos/chamada) | Speedup da GPU vs motor de CPU de 12 núcleos |
|---|---|
| 1 | 54.5x |
| 2 | 102.5x |
| 4 | 129.5x |
| 8 | 187.4x |
| 16 | 267.4x |
| 32 | 245.0x |
| 61 | 359.6x |
Isto é desenhado em medições reais. Em a GPU já está 54.5x à frente — porque esta comparação é contra o motor naive por barra, então o colapso algorítmico já vem embutido mesmo com um combo — mas está longe do seu teto: o overhead fixo ainda domina uma chamada de um combo só. Dobre o batch e o speedup quase dobra para 102.5x; em está em 267.4x; em está em 359.6x e visivelmente ainda subindo. A vantagem cresce com o tamanho do problema. Essa é a frase mais importante sobre GPUs e sweeps de parâmetros, e é exatamente o oposto de como speedups de GPU costumam ser citados — como se fossem uma propriedade constante do chip.
Duas notas de honestidade, porque esta é a série Backtests sem ilusões e uma tabela monótona e limpa sempre merece suspeita.
Primeiro, a queda: marca 245.0x, abaixo dos 267.4x em . Isso não é ruído a ser varrido para debaixo do tapete — é um artefato de fronteira de chunk. Nosso conv1d empacota 32 comprimentos em um chunk de kernel, então preenche exatamente um chunk sem folga, enquanto transborda para um segundo chunk que, por acaso, satura melhor o dispositivo. O ponto do roofline é a tendência, e um dispositivo real tem degraus de quantização; reportamos a oscilação em vez de selecionar a dedo os pontos que a escondem.
Segundo, e mais importante: 54.5x e 359.6x são ambos contra o motor de CPU naive, e nenhum dos dois é a vitória de hardware. Os dois números ainda contêm o colapso algorítmico de 27x. Se você trocasse a baseline de CPU pelo algoritmo colapsado rodando na CPU — mesma fórmula, fp64, os dois lados otimizados — cada linha encolheria aproximadamente por esse fator. Que é precisamente a decomposição que a próxima seção torna exata.
A decomposição honesta: 27x de algoritmo vezes 6.2x de hardware

Para separar o algoritmo do silício, é preciso medir três caminhos na mesma grade multi-timeframe, não dois. Então o benchmark roda:
- cpu-engine — o motor numba de produção, HMA alinhada por barra em todos os núcleos. A baseline naive-porém-paralela.
- cpu-collapsed — os vetores de pesos colapsados,
np.correlatemais gather, fp64, na CPU. Mesmo hardware que (1), algoritmo melhor. - gpu-mlx — os pesos colapsados como um
conv1dem batch na GPU Metal, fp32. Mesmo algoritmo que (2), hardware diferente.
Alinhe os três e a manchete multi-timeframe completa fatora de forma limpa:
O fator da esquerda, 27x, é o algoritmo — o colapso de por-barra-para-convolução da seção anterior. Ele não tem nada a ver com a GPU. Implemente-o em numpy e a CPU do seu laptop fica 27x mais rápida nesta carga de trabalho pelo preço de um refactor. O fator da direita, 6.2x, é o hardware — a vitória honesta, em igualdade de condições, da GPU Metal sobre o mesmo algoritmo otimizado em doze núcleos de CPU. Esse 6.2x é a única parte para a qual você realmente precisava de uma GPU.
Esta é toda a moral do artigo enunciada como aritmética. Quando um benchmark de fornecedor, o README de uma biblioteca ou um colega empolgado mostra "167x na GPU", o reflexo deveria ser uma única pergunta: qual era a baseline de CPU? Se a baseline era a implementação naive — e quase sempre é, porque uma baseline lenta rende um slide melhor — então a maior parte da manchete é uma vitória algorítmica à qual a CPU também tinha direito, e só o resíduo é hardware. Aqui o resíduo é 6.2x. Uma citação de 167x exagera a contribuição do hardware em aproximadamente 27 vezes.
E repare como o próprio fator de hardware se moveu com o tamanho do problema. No sweep single-timeframe pequeno, a vitória verdadeira GPU-contra-melhor-CPU foi de 3.2x. Na pré-computação multi-timeframe maior, foi de 6.2x — os mesmos dois chips, quase o dobro da vantagem de hardware, puramente porque a carga de trabalho maior empurra mais para cima no roofline, rumo ao teto plano de computação da GPU, antes que a CPU consiga acompanhar. A vantagem de hardware também não é uma constante. É um ponto na mesma curva ascendente, e o jeito de se mover para a direita nessa curva é fazer o batch maior e o trabalho por combo mais rico.
Um guia de decisão: quão largo o sweep precisa ser?

Dobre o roofline de volta em uma decisão que você pode tomar antes de gastar dinheiro. A GPU compensa quando as duas condições do roofline valem ao mesmo tempo: seu sweep está à direita da crista de batch (, de modo que o overhead fixo de lançamento e transferência é amortizado), e seu trabalho por combo é limitado por computação (intensidade aritmética rica o bastante para alcançar o teto plano, não uma passada rasa em O(n)). Concretamente, a partir do que medimos:
- Algumas dezenas de combos de uma estratégia single-timeframe: dispense a GPU. Você está à esquerda da crista; a vitória honesta sobre o numba paralelo é de ~3.2x em um job que já leva um décimo de segundo. O gargalo não é o kernel, é tudo em volta dele.
- Milhares de combos, ou uma pré-computação genuinamente multi-timeframe / multi-indicador: a GPU faz por merecer o seu lugar. O overhead se amortiza, as convoluções compartilhadas elevam a intensidade aritmética, e a vitória de hardware sobe para 6.2x e continua subindo com o batch. Este é o regime em que uma GPU transforma um sweep que rodaria a noite inteira em uma pausa para o café.
- Suba a escada da CPU primeiro — é mais barato e vem primeiro. O 298x na CPU e o colapso algorítmico de 27x são gratuitos ou quase, e são pré-requisitos, não alternativas: o 6.2x da GPU vem por cima do algoritmo colapsado, que você teria de escrever de qualquer forma. Uma GPU aparafusada em um pipeline naive mede, sobretudo, a ingenuidade.
Há também um imposto no ramo da GPU que não tem nada a ver com velocidade, e você precisa incluí-lo no preço: a GPU Metal da Apple não tem fp64 nenhum. Tudo roda em fp32, precisão relativa de ~1.2e-7. Isso mata o truque de livro-texto para médias móveis rápidas — a WMA por soma de prefixos em O(n) — porque, em uma escala de preço perto de 30,000 ao longo de 150k barras, as somas acumuladas chegam a ~1e14, sete ordens de magnitude além da faixa segura de inteiros do fp32; medimos erros relativos de até ~2e2 (um fator de duzentos, não dois por cento). A formulação que funciona é a convolução direta em janelas, onde cada soma de janela é um número limitado de termos de tamanho comparável e o fp32 se mantém preciso até ~8e-7. Mesmo assim, uma estratégia que decide pelo sinal de hma - hma3 vai ocasionalmente inverter um cruzamento em uma barra limítrofe onde as duas curvas quase se tocam, porque o arredondamento do fp32 desempata um quase-empate. É por isso que o caminho da GPU vem acompanhado de um gate de equivalência que mede o quanto os trades divergiram — delta de PnL em basis points, deslocamento relativo na contagem de trades — em vez de afirmar uma saída bit-idêntica que ele nunca poderá entregar. Na nossa execução, essa divergência foi de 90 fills deslocados em 479,016 (0.019%), bem dentro da tolerância, mas o fardo é real: ir para a GPU significa assumir uma história de paridade numérica, não apenas um clock mais rápido. Esse custo de engenharia também faz parte do break-even.
Os números são moldados pela Apple; a curva não
Cada número acima é um Apple M2 Max: um dispositivo de memória unificada, onde GPU e CPU compartilham um único pool, e uma GPU só-fp32, sem precisão dupla. Uma placa discreta NVIDIA ou AMD muda as constantes, e vale a pena ser explícito sobre em que direção cada uma se move, porque a forma do argumento sobrevive mesmo quando os números não.
- O overhead de transferência fica pior, não melhor. Uma placa discreta fica atrás do PCIe, então entradas e resultados fazem uma cópia real através do barramento que a memória unificada evita. Isso empurra a crista de batch para a direita — você precisa de um sweep ainda mais largo antes que uma GPU discreta amortize seu lançamento. A borda esquerda do roofline é mais íngreme em um dispositivo PCIe, não mais suave.
- O teto plano fica mais alto. Uma GPU de data center tem muito mais FLOP/s e largura de banda do que uma integrada, então a vitória assintótica de hardware em um sweep que satura o dispositivo é maior do que os nossos 6.2x. A recompensa por alcançar o lado direito da curva cresce; o pedágio por ficar sentado à esquerda cresce também.
- O fp64 volta, e com ele o truque das somas de prefixos. Em uma placa com precisão dupla de verdade, a WMA por soma de prefixos em O(n) volta a ser viável e o gate de paridade pode ser apertado rumo à exatidão bit a bit. O imposto específico de fp32 que pagamos — convolução direta em vez de somas de prefixos, um gate que mede divergência em vez de um assert — é um detalhe do Apple Silicon, não uma lei.
Nada disso muda a tese. Em qualquer dispositivo, : um overhead fixo que você precisa amortizar, uma assíntota da qual você só se aproxima pela direita. As constantes são hardware; a curva é aritmética. Meça seus próprios , e com um sweep de batch de cinco linhas antes de confiar na manchete de quem quer que seja — inclusive na nossa.
Onde isto se conecta
Esta é a quarta medição de uma pequena subsérie sobre de onde a velocidade de backtest realmente vem, e as peças se encaixam como um único argumento sobre o que otimizar antes de gastar:
- A escada de velocidade subiu de pandas a numba paralelo por 298x só na CPU, e deixou a GPU como pergunta em aberto. Este artigo a responde: a GPU é um quinto degrau real, mas condicional, que vale 3.2x–6.2x sobre o degrau mais alto da CPU, e apenas quando o sweep é largo o bastante para escalar o roofline.
- O imposto do IPC fez o mesmo movimento na direção oposta — medindo o que custa sair do processo — e chegou à mesma forma de conclusão: a fronteira (um socket, um lançamento de GPU) é barata; o imposto está em quantas vezes e de forma quão chatty você a atravessa. Faça batch das suas chamadas de GPU pela mesma razão pela qual você faz batch do seu IPC: para amortizar um custo fixo por travessia.
- O cache parquet agregado é a versão do lado da CPU do que a pré-computação na GPU faz — computar os indicadores compartilhados uma vez e reutilizá-los em cada combo. A GPU apenas leva esse princípio de reutilizar-e-fazer-batch até o silício.
- E o gate de paridade fp32 é um problema de paridade backtest-live em miniatura: no momento em que seu caminho rápido computa algo ligeiramente diferente da sua referência, você deve uma prestação de contas quantificada da divergência, não um aceno de mão.
A disciplina que conecta tudo é a mesma que esta série inteira defende: meça a coisa que estão de fato te vendendo. Um speedup é uma razão, e uma razão tem numerador e denominador. A maior parte da decepção com GPU vem de um denominador — a baseline de CPU — escolhido para lisonjear, e a maior parte do desperdício com GPU vem de rodar um sweep pequeno demais para sair da borda esquerda do roofline.
Conclusões
- Um speedup de GPU é uma curva, não um número. Na nossa pré-computação multi-timeframe, a vantagem sobre a CPU foi de 54.5x com um combo por chamada a 359.6x com sessenta e um — mesmo chip, mesmos dados. Qualquer número isolado é um ponto nessa curva; pergunte em que tamanho de batch ele foi medido.
- Sempre interrogue a baseline de CPU. A manchete multi-timeframe de 167x se fatora de forma limpa em 27x de algoritmo (um colapso de por-barra-para-convolução, que acelera a CPU na mesma medida) vezes 6.2x de hardware de verdade. A vitória justa GPU-contra-melhor-CPU aqui é 3.2x em single-timeframe, 6.2x em multi — não 167x.
- A vantagem cresce com o tamanho do problema, e o fator de hardware também. Batch maior e trabalho por combo mais rico empurram você para cima no roofline: a própria vitória honesta de hardware subiu de 3.2x para 6.2x puramente por tornar a carga de trabalho maior. Sweeps pequenos ficam à esquerda da crista e mal se beneficiam.
- Conserte o algoritmo e suba a escada da CPU primeiro — a vitória da GPU se apoia sobre eles, não os substitui. O 6.2x é medido contra o algoritmo colapsado que você teria de escrever de qualquer forma. Aparafuse uma GPU em um pipeline naive e a maior parte do que você mede é a ingenuidade, não o silício.
- Ir para a GPU significa assumir uma história de paridade numérica. Sem fp64 no Metal, o truque da WMA por soma de prefixos morre na escala de preço (erro relativo de ~2e2), e estratégias de sinal-de-cruzamento invertem em barras limítrofes. Entregue um gate de equivalência que quantifique a divergência em basis points; conte esse custo de engenharia no seu break-even.
Quando alguém te diz que a GPU deixou o backtest dele cem vezes mais rápido, essa pessoa não te disse quase nada. Pergunte o tamanho do batch e a baseline de CPU, e as cem vezes normalmente se resolvem em uma vitória de hardware de um dígito embrulhada em uma vitória algorítmica que ela poderia ter tido de graça — que vale a pena ter, num sweep grande o bastante, exatamente pelas razões que o roofline diz e por nenhuma razão a mais.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.