Práticas recomendadas para alocação / inicialização de memória multicore portátil / NUMA


17

Quando cálculos limitados de largura de banda de memória são executados em ambientes de memória compartilhada (por exemplo, encadeados via OpenMP, Pthreads ou TBB), há um dilema de como garantir que a memória seja distribuída corretamente na memória física , de modo que cada encadeamento acesse a memória em um barramento de memória "local". Embora as interfaces não sejam portáveis, a maioria dos sistemas operacionais possui maneiras de definir a afinidade do encadeamento (por exemplo, pthread_setaffinity_np()em muitos sistemas POSIX, sched_setaffinity()no Linux, SetThreadAffinityMask()no Windows). Também existem bibliotecas, como hwloc, para determinar a hierarquia de memória, mas, infelizmente, a maioria dos sistemas operacionais ainda não fornece maneiras de definir políticas de memória NUMA. Linux é uma exceção notável, com libnumapermitindo que o aplicativo manipule a diretiva de memória e a migração de páginas com granularidade de páginas (na linha principal desde 2004, portanto amplamente disponível). Outros sistemas operacionais esperam que os usuários observem uma política implícita de "primeiro toque".

Trabalhar com uma política de "primeiro toque" significa que o chamador deve criar e distribuir threads com qualquer afinidade que planeja usar posteriormente ao gravar na memória recém-alocada. (Pouquíssimos sistemas são configurados de modo que malloc()encontrem páginas, eles apenas prometem encontrá-las quando estão com falhas, talvez por threads diferentes.) Isso implica que a alocação usando calloc()ou inicializando imediatamente a memória após a alocação memset()é prejudicial, pois tenderá a falhar toda a memória no barramento de memória do núcleo executando o encadeamento de alocação, levando à pior largura de banda da memória quando a memória é acessada de vários encadeamentos. O mesmo se aplica ao newoperador C ++ , que insiste em inicializar muitas novas alocações (por exemplo,std::complex) Algumas observações sobre esse ambiente:

  • A alocação pode ser "coletiva de encadeamentos", mas agora a alocação se torna misturada no modelo de encadeamento, o que é indesejável para bibliotecas que talvez precisem interagir com clientes usando diferentes modelos de encadeamento (talvez cada um com seus próprios conjuntos de encadeamentos).
  • O RAII é considerado uma parte importante do C ++ idiomático, mas parece ser prejudicial ao desempenho da memória em um ambiente NUMA. O posicionamento newpode ser usado com memória alocada via malloc()ou rotinas de libnuma, mas isso altera o processo de alocação (que eu acredito que é necessário).
  • Edição: minha declaração anterior sobre o operador newestava incorreta, ele pode suportar vários argumentos, consulte a resposta de Chetan. Acredito que ainda exista uma preocupação em obter bibliotecas ou contêineres STL para usar afinidade especificada. Vários campos podem ser compactados e pode ser inconveniente garantir que, por exemplo, seja std::vectorrealocado com o gerenciador de contexto correto ativo.
  • Cada encadeamento pode alocar e danificar sua própria memória privada, mas a indexação nas regiões vizinhas é mais complicada. (Considere um produto de vetor de matriz esparsa com uma partição de linha da matriz e vetores; a indexação da parte não proprietária de x requer uma estrutura de dados mais complicada quando x não é contíguo na memória virtual.)yAxxx

Alguma solução para alocação / inicialização do NUMA é considerada idiomática? Eu deixei de fora outras dicas críticas?

(Não quero que meus exemplos de C ++ impliquem ênfase nessa linguagem; no entanto, a linguagem C ++ codifica algumas decisões sobre gerenciamento de memória que uma linguagem como C não faz, portanto, tende a haver mais resistência ao sugerir que os programadores de C ++ façam essas as coisas de maneira diferente.)

Respostas:


7

Uma solução para esse problema que eu prefiro é desagregar threads e tarefas (MPI) no nível do controlador de memória, efetivamente. Ou seja, remova os aspectos NUMA do seu código, tendo uma tarefa por soquete da CPU ou controlador de memória e, em seguida, encadeando sob cada tarefa. Se você fizer dessa maneira, poderá vincular toda a memória a esse soquete / controlador com segurança por meio do primeiro toque ou de uma das APIs disponíveis, independentemente de qual thread realmente faz o trabalho de alocação ou inicialização. A passagem de mensagens entre soquetes geralmente é bastante otimizada, no mínimo no MPI. Você sempre pode ter mais tarefas MPI do que isso, mas devido aos problemas que você levanta, eu raramente recomendo que as pessoas tenham menos.


11
Essa é uma solução prática, mas, apesar de estarmos rapidamente obtendo mais núcleos, o número de núcleos por nó NUMA é bastante estagnado em torno de 4. Portanto, no hipotético nó de 1000 núcleos, estaremos executando processos de 250 MPI? (Isso seria ótimo, mas eu sou cético.)
Jed Brown

Não concordo que o número de núcleos por NUMA esteja estagnado. Sandy Bridge E5 tem 8. Magny Cours tinha 12. Eu tenho um nó Westmere-EX com 10. Interlagos (ORNL Titan) tem 20. Knights Corner terá mais de 50. Eu acho que os núcleos por NUMA estão mantendo ritmo com a Lei de Moore, mais ou menos.
Bill Barth

Magny Cours e Interlagos têm duas matrizes em diferentes regiões NUMA, portanto, 6 e 8 núcleos por região NUMA. Volte a 2006, onde dois soquetes de Clovertown de quatro núcleos compartilhariam a mesma interface (chipset Blackford) na memória e não me parece que o número de núcleos por região NUMA esteja crescendo tão rapidamente. O Blue Gene / Q amplia um pouco mais essa visão plana da memória e talvez o Knight's Corner dê outro passo (embora seja um dispositivo diferente, talvez devamos comparar as GPUs, onde temos 15 (Fermi) ou agora 8 ( SMs que visualizam memória plana).
Jed Brown

Boa chamada para os chips AMD. Eu esqueci. Ainda assim, acho que você verá um crescimento contínuo nessa área por algum tempo.
Bill Barth

6

Esta resposta é uma resposta a dois equívocos relacionados ao C ++ na pergunta.

  1. "O mesmo se aplica ao novo operador C ++, que insiste em inicializar novas alocações (incluindo PODs)"
  2. "O operador C ++ new usa apenas um parâmetro"

Não é uma resposta direta para problemas com vários núcleos mencionados. Apenas respondendo aos comentários que classificam os programadores de C ++ como fanáticos de C ++ para que a reputação seja mantida;).

Para apontar 1. C ++ "novo" ou alocação de pilha não insiste em inicializar novos objetos, sejam PODs ou não. O construtor padrão da classe, conforme definido pelo usuário, tem essa responsabilidade. O primeiro código abaixo mostra lixo impresso se a classe é POD ou não.

Para o ponto 2. O C ++ permite sobrecarregar "new" com vários argumentos. O segundo código abaixo mostra esse caso para alocar objetos únicos. Deve dar uma idéia e talvez seja útil para a situação que você tem. operador new [] também pode ser modificado adequadamente.

// Código do ponto 1.

#include <iostream>

struct A
{
    // int/double/char/etc not inited with 0
    // with or without this constructor
    // If present, the class is not POD, else it is.
    A() { }

    int i;
    double d;
    char c[20];
};

int main()
{
    A* a = new A;
    std::cout << a->i << ' ' << a->d << '\n';
    for(int i = 0; i < 20; ++i)
        std::cout << (int) a->c[i] << '\n';
}

O compilador 11.1 da Intel mostra essa saída (que é obviamente a memória não inicializada apontada por "a").

993001483 6.50751e+029
105
108
... // skipped
97
108

// Código para o ponto 2.

#include <cstddef>
#include <iostream>
#include <new>

// Just to use two different classes.
class arena { };
class policy { };

struct A
{
    void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
    {
        std::cout << "special operator new\n";
        return (void*)0x1234; //Just to test
    }
};

void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
{
    std::cout << "special operator new (global)\n";
    return (void*)0x5678; //Just to test
}

int main ()
{
    arena arena_obj;
    policy policy_obj;
    A* ptr = new(arena_obj, policy_obj) A;
    int* iptr = new(arena_obj, policy_obj) int;
    std::cout << ptr << "\n";
    std::cout << iptr << "\n";
}

Obrigado pelas correções. Parece que o C ++ não apresenta complicações adicionais em relação ao C, exceto para matrizes não-POD, como as std::complexque são explicitamente inicializadas.
Jed Brown

11
@JedBrown: Razão número 6 para evitar o uso std::complex?
22812 Jack Poulson

1

Temos a infraestrutura de software para paralelizar a montagem em cada célula em vários núcleos usando os Threading Building Blocks (em essência, você tem uma tarefa por célula e precisa agendá-las nos processadores disponíveis - não é assim que é implementado, mas é a ideia geral). O problema é que, para a integração local, você precisa de vários objetos temporários (scratch) e precisa fornecer pelo menos o número de tarefas que podem ser executadas em paralelo. Vemos pouca aceleração, presumivelmente porque quando uma tarefa é colocada em um processador, ela pega um dos objetos temporários que normalmente estarão no cache de algum outro núcleo. Tivemos duas perguntas:

(i) Essa é realmente a razão? Quando executamos o programa no cachegrind, vejo que estou usando basicamente o mesmo número de instruções que ao executar o programa em um único encadeamento, mas o tempo total de execução acumulado em todos os encadeamentos é muito maior que o do encadeamento único. É realmente porque eu continuamente falha no cache?

(ii) Como posso descobrir onde estou, onde estão cada um dos objetos arranhados e qual objeto arranhado eu precisaria para acessar o que está quente no cache do meu núcleo atual?

Por fim, não encontramos respostas para nenhuma dessas soluções e, após alguns trabalhos, decidimos que não possuímos as ferramentas para investigar e resolver esses problemas. Eu sei como, pelo menos em princípio, resolver o problema (ii) (ou seja, usando objetos locais do encadeamento, assumindo que os encadeamentos permaneçam presos aos núcleos do processador - outra conjectura que não é trivial para testar), mas não tenho ferramentas para testar o problema (Eu).

Portanto, da nossa perspectiva, lidar com o NUMA ainda é uma questão não resolvida.


Você deve vincular seus threads aos soquetes para não precisar se perguntar se os processadores estão fixados. O Linux gosta de mudar as coisas.
Bill Barth

Além disso, a amostragem de getcpu () ou sched_getcpu () (dependendo da sua libc e kernel e outros enfeites) deve permitir que você determine onde os threads estão em execução no Linux.
Bill Barth

Sim, e acho que os Threading Building Blocks que usamos para agendar o trabalho em threads pinos threads em processadores. É por isso que tentamos trabalhar com o armazenamento local de threads. Mas ainda é difícil para mim encontrar uma solução para o meu problema (i).
Wolfgang Bangerth

1

Além do hwloc, existem algumas ferramentas que podem relatar no ambiente de memória de um cluster HPC e que podem ser usadas para definir uma variedade de configurações NUMA.

Eu recomendaria o LIKWID como uma dessas ferramentas, pois evita uma abordagem baseada em código, permitindo, por exemplo, fixar um processo em um núcleo. Essa abordagem de ferramentas para abordar a configuração de memória específica da máquina ajudará a garantir a portabilidade do seu código entre os clusters.

Você pode encontrar uma breve apresentação descrevendo-a no ISC'13 " LIKWID - Lightweight Performance Tools " e os autores publicaram um artigo sobre o Arxiv " Melhores práticas para engenharia de desempenho assistida por HPM no moderno processador multicore ". Este documento descreve uma abordagem para interpretar os dados dos contadores de hardware para desenvolver código de desempenho específico para a arquitetura e a topologia de memória da sua máquina.


O LIKWID é útil, mas a questão era mais sobre como escrever bibliotecas numéricas / sensíveis à memória que possam obter e auto-auditar com segurança a localidade esperada em diversos ambientes de execução, esquemas de encadeamento, gerenciamento de recursos MPI e definição de afinidade, usados ​​com outras bibliotecas, etc.
Jed Brown
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.