Xgboost: Pedido de contribuição: melhore o desempenho da CPU multi-core de 'hist'

Criado em 19 out. 2018  ·  44Comentários  ·  Fonte: dmlc/xgboost

É hora de enfrentar o elefante na sala: desempenho em CPUs multi-core.

Descrição do problema

Atualmente, o algoritmo de crescimento de árvore hist ( tree_method=hist ) não é bem escalado em CPUs multi-core: para alguns conjuntos de dados, o desempenho se deteriora conforme o número de threads aumenta . Este problema foi descoberto pelo Gradient Boosting Benchmark de @ Laurae2 ( GitHub ).

O comportamento de dimensionamento é o seguinte para o conjunto de dados da Bosch :
Performance scaling on C5.9xlarge

Chamada de contribuição

Eu identifiquei o gargalo de desempenho do algoritmo 'hist' e o coloquei em um pequeno repositório: hcho3 / xgboost-fast-hist-perf-lab . Você pode tentar melhorar o desempenho revisando src / build_hist.cc .

Algumas ideias

  • Altere o layout da matriz de dados de CSR para outros layouts, como ellpack
  • Tente distribuir o trabalho de forma mais igualitária entre os threads de trabalho. O desequilíbrio de trabalho é causado por padrões esparsos irregulares da matriz de dados.
  • Agrupe recursos complementares, um cenário comum para dados codificados em um único momento.
help wanted roadmap

Comentários muito úteis

O dimensionamento de vários núcleos e, na verdade, também o problema de NUMA foi bastante melhorado:

Multicore:

Screen Shot 2020-09-17 at 12 37 55 AM

Muito notável a melhoria em dados menores (linhas de 0,1 milhões)

Screen Shot 2020-09-17 at 12 43 26 AM
Screen Shot 2020-09-17 at 12 43 34 AM

Mais detalhes aqui:

https://github.com/szilard/GBM-perf#multi -core-scaling-cpu
https://github.com/szilard/GBM-perf/issues/29#issuecomment -689713624

Além disso, o problema NUMA foi amplamente mitigado:

Screen Shot 2020-09-17 at 12 46 49 AM

Screen Shot 2020-09-17 at 12 48 23 AM
Screen Shot 2020-09-17 at 12 48 32 AM

Todos 44 comentários

@ Laurae2 Obrigado por preparar o benchmark GBT. Tem sido útil para identificar o local do problema.

@ hcho3 O OpenMP guided schedule ajuda no balanceamento de carga? Nesse caso, o ellpack não será muito útil.

Meu palpite é que a alocação estática de trabalho usando ellpack alcançaria uma carga de trabalho balanceada com sobrecarga menor que guided ou dynamic modo OpenMP. Com dynamic , você obtém sobrecarga de tempo de execução para manter a fila de roubo de trabalho

pode estar um pouco fora do assunto, temos resultados de benchmark de approx ?

Descobrimos a aceleração abaixo do ideal com multithreading em nosso ambiente interno ... queremos olhar os dados de outras pessoas

@CodingCat O conjunto de benchmarks vinculado usa apenas hist . approx mostra degradação de desempenho como hist (por exemplo, 36 threads mais lentos do que 3 threads)?

@ hcho3 devido à limitação em nosso cluster, podemos testar apenas com até 8 threads ... mas encontramos aceleração muito limitada comparando 8 a 4 .....

@CodingCat Você quer dizer que 8 threads são executados mais lentamente do que 4?

@CodingCat approx tem um dimensionamento tão pobre que eu nem queria tentar compará-lo. Nem mesmo escalona adequadamente no meu laptop de 4 núcleos (3,6 GHz), portanto, nem imagino com 64 ou 72 threads.

@ hcho3 Vou dar uma olhada nele usando seu repositório com VTune mais tarde.


Para aqueles que desejam obter um desempenho detalhado no VTune, pode-se usar o seguinte para adicionar ao cabeçalho:

#include <ittnotify.h> // Intel Instrumentation and Tracing Technology

Adicione o seguinte antes do que deseja rastrear fora de um loop (renomeie as strings / variáveis):

__itt_resume();
__itt_domain* domain = __itt_domain_create("MyDomain");
__itt_string_handle* task = __itt_string_handle_create("MyTask");
__itt_task_begin(domain, __itt_null, __itt_null, task);

Adicione o seguinte após o que você deseja acompanhar fora de um loop (renomeie as strings / variáveis):

__itt_task_end(domain);
__itt_pause();

E inicie um projeto com VTune com os parâmetros corretos para o número de threads. Inicie o executável com a instrumentação pausada para fazer a análise de desempenho.

@ hcho3 não é mais lento, mas talvez apenas 15-% de aceleração com mais 4 threads ... (se eu conduzir mais experimentos, eu suspeitaria que os resultados convergiriam .....

@ Laurae2 parece que não sou a única

@ hcho3 Vou tentar obter alguns resultados de escala antes do final desta semana, se ninguém fizer em exact , approx e hist , todos com depth=6 , no commit e26b5d6.

Eu migrei recentemente meu servidor de computação e estou refazendo novos benchmarks no Bosch em uma nova máquina com 3,7 GHz turbo / 36 núcleos / 72 threads / 80 GBps de largura de banda de RAM esta semana.

O atualizador fast_hist deve ser muito mais rápido para xgboost distribuído. @CodingCat Estou surpreso que ninguém tenha tentado adicionar chamadas AllReduce para que funcione no modo distribuído.

@RAMitchell Eu era muito novo quando escrevi o atualizador fast_hist, por isso não tem suporte para modo distribuído. Eu gostaria de obtê-lo após o lançamento de 0,81.

@ Laurae2 Para hist parecem ser consistentes com seus resultados anteriores. Posso colocar os números, se quiser.

@ Laurae2 Além disso, tenho acesso às máquinas EC2. Se você tiver um script que gostaria de executar em uma instância EC2, me avise.

@RAMitchell Eu era muito novo quando escrevi o atualizador fast_hist, por isso não tem suporte para modo distribuído. Eu gostaria de obtê-lo após o lançamento de 0,81.

@ hcho3 se você não se importa, posso aceitar o desafio de obter o algoritmo de histograma mais rápido distribuído. Atualmente, estou metade do tempo nele no meu trabalho no Uber e no próximo ano posso ter mais tempo no xgboost

@CodingCat Isso seria ótimo, obrigado! Avise-me se tiver alguma dúvida sobre o código 'hist'.

@CodingCat FYI, pretendo adicionar testes de unidade para o atualizador 'hist' logo após o lançamento de 0.81. Isso deve ajudar quando se trata de adicionar suporte distribuído.

@ hcho3 @CodingCat approx parece ter sido removido no último mês, é um comportamento esperado?

https://github.com/dmlc/xgboost/commit/70d208d68c3a32aaa4fcd6aa456f286a4da5912f#diff -53a3a623be5ce5a351a89012c7b03a31 (PR https://github.com/dmlc/xgboost tree_method = approx pull / 33 removeu = idêntico

@ Laurae2 Parece que o refatorador removeu uma mensagem INFO sobre approx sendo selecionada. Caso contrário, approx ainda deve estar disponível.

@ Laurae2 Na verdade, você está certo. Mesmo que approx ainda esteja na base de código, por alguma razão ele não está sendo invocado mesmo quando tree_method=approx está definido. Vou investigar esse bug o mais rápido possível.

O problema # 3840 foi arquivado. A versão 0.81 não será lançada até que isso seja corrigido.

@ hcho3 Estou achando algo muito estranho no meu servidor com histograma rápido, informarei os resultados se amanhã o cálculo do benchmark terminar (estamos falando sobre a enorme eficiência negativa do histograma rápido, é tão grande que estou tentando para medi-lo, mas espero que não fique muito longo).

Por aproximadamente, a baixa eficiência é muito melhor do que o esperado, mas não espero que seja verdade para qualquer computador (talvez fique melhor com a geração de CPU Intel mais recente = maior frequência de RAM?). Vou postar os dados assim que o histograma rápido terminar no meu servidor.

Para obter informações, estou usando o conjunto de dados da Bosch com 477 recursos (os recursos com menos de 5% de valores ausentes).

Atingiu mais de 3.000 horas de tempo de CPU ... (pelo menos meu servidor está bem aproveitado por um tempo) a seguir, para mim, será consultar https://github.com/hcho3/xgboost-fast-hist-perf- lab / blob / master / src / build_hist.cc com Intel VTune.

@ hcho3 Se você quiser, posso fornecer meu script R de referência assim que meu servidor terminar de calcular. Corri com depth=8 e nrounds=50 , para todos tree_method=exact , tree_method=approx (com updater=grow_histmaker,prune solução alternativa, antes de # 3849), e tree_method=hist , de 1 a 72 tópicos. Ele pode descobrir coisas mais interessantes para trabalhar (e você também seria capaz de testá-lo na AWS).

Por favor, veja os resultados preliminares abaixo, executados 7 vezes para resultados médios. Certifique-se de clicar para ver melhor. Mesa sintética fornecida. Ao contrário dos programas de plotagem, as CPUs não foram fixadas.

Os gráficos parecem claramente diferentes daqueles para os quais eu estava preparado ... (devido ao quão estranho é o comportamento, estou executando novamente com UMA ligado (NUMA desligado)). Mais tarde, verificarei com o Intel VTune.

Hardware e Software:

  • CPU: Dual Xeon Gold 6154, 3,7 GHz turbo, 2x 18 núcleos / 36 threads
  • RAM: 4x 64 GB de RAM DDR4 2666 MHz (canal duplo, largura de banda de aproximadamente 80 GBps)
  • BIOS: NUMA ativado, agrupamento Sub NUMA desativado
  • Sistema operacional: Pop_OS! 18,10
  • Governador: desempenho
  • Kernel: 4.18.0-10
  • Sinalizadores de kernel: pti=off spectre_v2=off spec_store_bypass_disable=off l1tf=off noibrs noibpb nopti no_stf_barrier
  • Compilador: gcc 8.2.0
  • R: 3.5.1 compilado com gcc 8.2.0 e com Intel MKL
  • sinalizadores de compilação adicionais em R: -O3 -mtune=native

Proteções de fusão / espectro:

laurae@laurae-compute:~$ head /sys/devices/system/cpu/vulnerabilities/*
==> /sys/devices/system/cpu/vulnerabilities/l1tf <==
Mitigation: PTE Inversion; VMX: vulnerable

==> /sys/devices/system/cpu/vulnerabilities/meltdown <==
Vulnerable

==> /sys/devices/system/cpu/vulnerabilities/spec_store_bypass <==
Vulnerable

==> /sys/devices/system/cpu/vulnerabilities/spectre_v1 <==
Mitigation: __user pointer sanitization

==> /sys/devices/system/cpu/vulnerabilities/spectre_v2 <==
Vulnerable

| Tópicos Exato (eficiência) | Aprox (eficiência) | Hist (eficiência) |
| ---: | ---: | ---: | ---: |
| 1 | 1367s (100%) | 1702s (100%) | 69,9s (100%) |
| 2 | 758,7s (180%) | 881,0s (193%) | 52,5 s (133%) |
| 4 368,6s (371%) | 445,6s (382%) | 31,7s (221%) |
| 6 241,5 s (566%) | 219,6s (582%) | 24,1 s (290%) |
| 9 160,4s (852%) | 194,4 s (875%) | 23,1 s (303%) |
| 18 86,3 s (1583%) | 106,3s (1601%) | 24,2s (289%) |
| 27 66,4s (2059%) | 80,2s (2122%) | 63,6s (110%) |
| 36 52,9 s (2586%) | 60,0s (2837%) | 55,2s (127%) |
| 54 215,4s (635%) | 289,5s (588%) | 343,0s (20%) |
| 72 218,9s (624%) | 295,6s (576%) | 1237,2s (6%) |

Velocidade exata xgboost:
image

xgboost Eficiência exata:
image

xgboost Velocidade aproximada:
image

xgboost Eficiência aproximada:
image

Velocidade do histograma xgboost:
image

Eficiência do histograma xgboost:
image

Parece um problema com vários soquetes.

@RAMitchell parece ser um problema com a disponibilidade de nós NUMA, posso replicar esse problema (com um resultado muito pior com menos threads durante o treinamento) usando Clustering Sub NUMA (2 soquetes = 4 nós NUMA em vez de 1 soquete = 2 nós NUMA )

image

O xgboost, como a maioria dos algoritmos de aprendizado de máquina, não tem otimização para lidar com nós NUMA. Mas isso seria um segundo problema. Portanto, eles não são apropriados para ambiente de vários soquetes nem quando nós NUMA estão disponíveis por meio de COD (Cluster on Die) ou SNC (Sub NUMA Clustering), e o hyperthreading torna o desequilíbrio da carga de trabalho uma grande penalidade para eles.

O problema 1 seria sobre a grande degradação do desempenho de multithread no modo xgboost hist (esse problema).

O problema 2 seria sobre a otimização NUMA (outro problema para abrir).

Aqui estão os resultados com NUMA desativado. Emparelhei os resultados com NUMA habilitado para comparação. Também foram adicionados 71 threads para mostrar o desempenho antes que a CPU fique sobrecarregada com o agendador do kernel em 72 threads (mais recursos necessários do que disponíveis).

UMA se sai muito melhor do que NUMA para multithreading, este é um resultado esperado de intercalação de memória em um processo não ciente de NUMA.


Tempo tempo:

| Tópicos Exato
NUMA | Exato
UMA | Aproximadamente
NUMA | Aproximadamente
UMA | Hist
NUMA | Hist
UMA |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 1367s | 1667 | 1702s | 1792 | 69.9s | 85.6s |
| 2 | 758,7s | 810.3s | 881.0s | 909.0s | 52.5s | 54.1s |
| 4 368,6s | 413.0s | 445.6s | 452.9s | 31,7s | 36,2s |
| 6 241.5s | 273.8s | 219.6s | 302.4s | 24.1s | 30.5s |
| 9 160,4s | 182.8s | 194.4s | 202,5s | 23.1s | 28.3s |
| 18 86.3s | 94,4s | 106,3s | 105.8s | 24,2s | 31,2s |
| 27 66,4s | 66,4s | 80.2s | 73.6s | 63.6s | 37.5s |
| 36 52.9s | 52,7s | 60.0s | 59,4s | 55,2s | 43.5s |
| 54 215,4s | 49,2s | 289.5s | 58,5s | 343.0s | 57,4s |
| 71 218.3s | 47.01s | 295.9s | 56.5s | 1238.2s | 71.5s |
| 72 218.9s | 49.0s | 295.6s | 58,6s | 1237.2s | 79,1s |

Tabela de eficiência:

| Tópicos Exato
NUMA | Exato
UMA | Aproximadamente
NUMA | Aproximadamente
UMA | Hist
NUMA | Hist
UMA |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 100% | 100% | 100% | 100% | 100% | 100% |
| 2 | 180% | 206% | 193% | 197% | 133% | 158% |
| 4 371% | 404% | 382% | 396% | 221% | 236% |
| 6 566% | 609% | 582% | 593% | 290% | 280% |
| 9 852% | 912% | 875% | 885% | 303% | 302% |
| 18 1583% | 1766% | 1601% | 1694% | 289% | 274% |
| 27 2059% | 2510% | 2122% | 2436% | 110% | 229% |
| 36 2586% | 3162% | 2837% | 3017% | 127% | 197% |
| 54 635% | 3384% | 588% | 3065% | 20% | 149% |
| 71 626% | 3545% | 575% | 3172% | 6% | 120% |
| 72 624% | 3401% | 576% | 3059% | 6% | 108% |


Modo UMA.

Velocidade exata xgboost:
image

xgboost Eficiência exata:
image

xgboost Velocidade aproximada:
image

xgboost Eficiência aproximada:
image

Velocidade do histograma xgboost:
image

Eficiência do histograma xgboost:
image

Conforme comentado em https://github.com/dmlc/xgboost/pull/3957#issuecomment -453815876, testei os commits a2dc929 (pré-melhoria da CPU) e 5f151c5 (pós-melhoria da CPU).

Eu testei usando meu servidor Dual Xeon 6154 (compilador gcc, não Intel), usando Bosch para 500 iterações, eta 0,10 e profundidade 8, com 3 execuções cada para 1 a 72 threads. Notamos um aumento de desempenho de cerca de 50% (1/3 mais rápido) para cargas de trabalho multithread com desempenho máximo.

Aqui estão os resultados para antes de # 3957 (commit a2dc929):

image

Aqui estão os resultados para # 3957 (commit 5f151c5):

image

Usando as curvas de eficiência, vemos o aumento de escalabilidade de 50% (isso não significa que o problema está resolvido: ainda temos que melhorá-lo, se pudermos - idealmente, se pudermos chegar à faixa de 1000-2000%, o que seria insanamente ótimo).

Curva de eficiência de a2dc929:

image

Curva de eficiência de 5f151c5:

image

Obrigado @ Laurae2 , irei em frente e

@ hcho3 @SmirnovEgorRu Estou vendo uma pequena regressão de desempenho da CPU em cargas de trabalho de thread único em dados 100% densos com o commit 5f151c5 que incorre em uma penalidade de 10% a 15% geral ao fazer o ajuste de hiperparâmetro em núcleos X x 1 thread xgboost.

Aqui está um exemplo de dados densos aleatórios de 50 milhões de linhas x 100 colunas (gcc 8), requer pelo menos 256 GB de RAM para treiná-lo adequadamente no Python / R, executado 3 vezes (6 dias).

Commit a2dc929:

image

Commit 5f151c5:

image

Embora eles levem a um desempenho multithread muito semelhante, o desempenho do single threaded é atingido por um treinamento mais lento (as melhorias de @SmirnovEgorRu ainda

Excluindo o tempo de criação gmat, temos para singlethread em 50M x 100:

| Comprometa-se | Total | tempo gmat | Tempo do trem |
| : --- | ---: | ---: | ---: |
| a2dc929 | 2926s | 816s | 2109s |
| 5f151c5 | (+ 13%) 3316s | (~%) 817s | (+ 18%) 2499s |

@ hcho3 @ Laurae2 Geralmente Hyper-threading ajuda apenas no caso de algoritmos vinculados ao núcleo, nenhum algoritmo vinculado à memória.
O HT ajuda a carregar o pipeline da CPU por meio de mais instruções de execução. Se a maioria das instruções esperar pela execução das instruções anteriores (limite de latência) - o HT pode realmente ajudar, em algumas cargas de trabalho específicas, observei acelerar em até 1,5x.
No entanto, se o seu aplicativo passa a maior parte do tempo trabalhando com memória (limite de memória) - o HT torna ainda pior. 2 hyper-threads compartilham um cpu-cache e deslocam informações úteis entre si. Como resultado, vemos degradação de desempenho.
Gradient Boosting - algoritmo de limite de memória. O uso de HT não deve trazer melhoria de desempenho em nenhum caso e sua aceleração máxima devido ao threading vs versão 1thread é limitada pelo número de núcleos de hardware. Então, minha opinião é melhor medir o desempenho no CPU sem HT.

E o NUMA - observei os mesmos problemas na implementação do DAAL. Requer controle de uso de memória por cada núcleo. Vou olhar para isso no futuro.

Que tal uma pequena lentidão em 1 thread - vou investigá-la. Eu acho - consertar é fácil.

@ hcho3 No momento, estou trabalhando na próxima parte das otimizações. Espero estar pronto para um novo pull-request em um futuro próximo.

@SmirnovEgorRu Obrigado novamente por seu esforço. Para sua informação, houve uma discussão recente sobre o aumento da quantidade de paralelismo ao realizar a expansão de nó por nível: # 4077.

@ Laurae2 Agora que

@ hcho3 Farei um novo teste mais tarde para verificar, mas pelo que pude notar, havia regressões de desempenho em ambientes de produção (especialmente # 3957 causando mais de 30x de lentidão).

Vou verificar os resultados de desempenho com @szilard também.

Abra o exemplo: https://github.com/szilard/GBM-perf/issues/9

O dimensionamento de vários núcleos e, na verdade, também o problema de NUMA foi bastante melhorado:

Multicore:

Screen Shot 2020-09-17 at 12 37 55 AM

Muito notável a melhoria em dados menores (linhas de 0,1 milhões)

Screen Shot 2020-09-17 at 12 43 26 AM
Screen Shot 2020-09-17 at 12 43 34 AM

Mais detalhes aqui:

https://github.com/szilard/GBM-perf#multi -core-scaling-cpu
https://github.com/szilard/GBM-perf/issues/29#issuecomment -689713624

Além disso, o problema NUMA foi amplamente mitigado:

Screen Shot 2020-09-17 at 12 46 49 AM

Screen Shot 2020-09-17 at 12 48 23 AM
Screen Shot 2020-09-17 at 12 48 32 AM

@szilard Muito obrigado por

Sim, ótimo trabalho a todos neste tópico por terem realizado isso.

Para sua informação, aqui estão os tempos de treinamento em linhas de 1 milhão em EC2 r4.16xlarge (2 soquetes com 16c + 16HT cada) em 1, 16 (1so e sem HT) e 64 (todos) núcleos para diferentes versões de xgboost:

Screen Shot 2020-09-17 at 11 11 50 AM

https://github.com/szilard/GBM-perf/issues/40

@szilard , muito obrigado pela análise! É bom saber que as otimizações funcionam.

PS Acima vejo que o XGB 1.2 tem alguma regressão em relação à versão 1.1. É uma informação muito interessante, deixe-me esclarecer isso. Não é esperado para mim.

@szilard , se este tópico for interessante para você - algumas informações e resultados das otimizações de CPU estão disponíveis neste blog:
https://medium.com/intel-analytics-software/new-optimizations-for-cpu-in-xgboost-1-1-81144ea21115

Obrigado @SmirnovEgorRu pelo seu trabalho de otimização e pelo link para o post do blog (não vi esse post antes).

Para ser mais fácil reproduzir meus números e obter novos no futuro e / ou outro hardware, criei um Dockerfile separado para isso:

https://github.com/szilard/GBM-perf/tree/master/analysis/xgboost_cpu_by_version

Você precisará definir os IDs de núcleo da CPU para o primeiro soquete, sem núcleos hiperencadeados (por exemplo, 0-15 em r4.16xlarge, que tem 2 soquetes, 16c + 16HT cada) e a versão xgboost:

VER=v1.2.0
CORES_1SO_NOHT=0-15    ## set physical core ids on first socket, no hyperthreading
sudo docker build --build-arg CACHE_DATE=$(date +%Y-%m-%d) --build-arg VER=$VER -t gbmperf_xgboost_cpu_ver .
sudo docker run --rm -e CORES_1SO_NOHT=$CORES_1SO_NOHT gbmperf_xgboost_cpu_ver

Pode valer a pena executar o script várias vezes, os tempos de treinamento em todos os núcleos geralmente mostram uma variabilidade um pouco maior, não tenho certeza se por causa do ambiente de virtualização (EC2) ou por causa do NUMA.

Resultados em c5.metal que tem maior frequência e mais núcleos do que r4.16xlarge que tenho usado no benchmark:

https://github.com/szilard/GBM-perf/issues/41

TLDR: xgboost tira o máximo proveito de mais núcleos e mais rápidos do que outras bibliotecas. 👍

Eu me pergunto sobre isso:

Screen Shot 2020-09-21 at 9 57 31 AM

a aceleração de 1 a 24 núcleos para xgboost é menor para os dados maiores (10 milhões de linhas, painéis à direita) do que para dados menores (1 milhão de linhas, painéis na coluna do meio). É algum tipo de aumento de ocorrências de cache ou algo que outras bibliotecas não têm?

Aqui estão alguns resultados na AMD:

https://github.com/szilard/GBM-perf/issues/42

Parece que as otimizações do xgboost também estão funcionando muito bem na AMD.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

uasthana15 picture uasthana15  ·  4Comentários

trivialfis picture trivialfis  ·  3Comentários

tqchen picture tqchen  ·  4Comentários

matthewmav picture matthewmav  ·  3Comentários

RanaivosonHerimanitra picture RanaivosonHerimanitra  ·  3Comentários