A memória RAM em servidores modernos não é uniforme nem homogênea. Em nós multissocket ou processadores modulares baseados em chiplets, acessar memória vinculada a outro nó custa entre 1,5× e 2,5× mais tempo do que acessar a memória DRAM local.
Essa discrepância física transforma o modelo clássico de multiprocessamento simétrico em um gargalo silencioso.
Sistemas que ignoram a topologia de hardware sofrem quedas severas de vazão e picos imprevisíveis de latência de cauda (tail latency p99).

Como o fim da memória uniforme redefiniu a microarquitetura de servidores
A memória uniforme ruiu quando o aumento de núcleos superou a capacidade física do barramento frontal compartilhado (Front Side Bus). Em arquiteturas contemporâneas, cada soquete ou grupo de núcleos possui canais de memória DRAM dedicados diretamente integrados ao silício.
No modelo SMP (Symmetric Multiprocessing) tradicional, todos os núcleos disputavam o mesmo controlador de memória.
Essa contenção elétrica limitava a escalabilidade dos servidores a poucos processadores.
Para romper esse limite físico, a indústria adotou a arquitetura NUMA (Non-Uniform Memory Access). Cada grupo de núcleos passou a formar um nó independente com seus próprios controladores integrados.
Processadores como AMD EPYC e Intel Xeon Scalable fragmentaram a memória em domínios locais.
| Métrica de Acesso à Memória | Acesso Local (Mesmo Nó NUMA) | Acesso Remoto (Nó NUMA Vizinho) | Penalidade Relativa |
|---|---|---|---|
| Latência de Leitura DRAM | 60 ns a 80 ns | 140 ns a 210 ns | 2,3× a 2,6× mais lento |
| Largura de Banda de Pico | 350 a 460 GB/s por soquete | Limitada pelo link (64 a 128 GB/s) | Queda de até 70% na vazão |
| Coerência de Cache (RFO) | Resolução local em cache L3 | Broadcast inter-socket via UPI/IFIS | Tráfego intenso de snooping |
| Consumo de Energia por Byte | ~3 a 5 pJ/bit | ~15 a 25 pJ/bit | 3× a 5× mais energia gasta |
Qual é o impacto real da latência de interconexão no pipeline da CPU
A travessia de barramentos de interconexão paralisa o pipeline da CPU por centenas de ciclos de clock adicionais durante faltas de cache L3. Quando um núcleo solicita dados alocados na memória de outro nó, o controlador precisa emitir mensagens de coerência através dos links físicos.
Conforme demonstrado por Ulrich Drepper em seu clássico ensaio What Every Programmer Should Know About Memory, a cache L1 entrega dados em 4 a 5 ciclos (cerca de 1 ns).
A memória cache L2 responde em cerca de 14 ciclos, e a cache L3 compartilhada opera em torno de 40 a 50 ciclos.
Quando ocorre uma perda de cache (cache miss) que demanda leitura remota, o atraso salta para mais de 500 ciclos de CPU.
O Reorder Buffer (ROB) esgota suas entradas especulativas antes que os dados cheguem da DRAM distante. O pipeline superescalar entra em bolha estrutural (pipeline stall).
Além da latência crua, operações de escrita que exigem posse exclusiva da linha de 64 bytes (Read For Ownership) saturam os filtros de coerência.

Por que a armadilha do first-touch no Linux derruba o paralelismo de aplicações
O kernel do Linux aloca páginas físicas de memória apenas no primeiro acesso de escrita, e não no momento da reserva virtual. Essa política faz com que estruturas inicializadas por uma única thread fiquem concentradas no mesmo nó NUMA.
Quando uma aplicação executa chamadas como malloc ou mmap, o sistema operacional reserva apenas espaço de endereçamento virtual.
Nenhuma página física de 4 KB ou 2 MB é mapeada na tabela de páginas (page table) nesse instante inicial.
A atribuição da memória física ocorre no manipulador de falha de página (page fault handler) do kernel, na função do_anonymous_page.
O kernel vincula a página física ao nó NUMA onde reside a thread que executou a primeira escrita.
Esse comportamento padrão do Linux, documentado nas Políticas de Memória NUMA do Kernel, cria a armadilha da inicialização monothread (Single-Thread Init Trap).
Se a thread principal zera um grande vetor de dados antes de despachar tarefas para as threads de trabalho, toda a memória física residirá em um único nó.
Quando as outras threads iniciam o processamento em núcleos remotos, todas as suas leituras e gravações cruzarão o barramento de interconexão.
// ARMADILHA: Toda a memória fica alocada no nó NUMA da thread principal
double *buffer = (double *)malloc(N * sizeof(double));
#pragma omp parallel for
for (size_t i = 0; i < N; i++) {
buffer[i] = 0.0; // Incorreto se executado antes da fixação das threads
}
// CORRETO: Cada thread de trabalho toca e inicializa sua própria fatia local
#pragma omp parallel
{
int tid = omp_get_thread_num();
size_t chunk = N / omp_get_num_threads();
size_t start = tid * chunk;
size_t end = (tid == omp_get_num_threads() - 1) ? N : start + chunk;
for (size_t i = start; i < end; i++) {
buffer[i] = 0.0; // Mapeia páginas físicas no nó NUMA de cada worker
}
}
Como configurar afinidade de CPU e políticas de alocação no Linux
O controle determinístico de afinidade exige vincular processos e threads a núcleos específicos e definir políticas estritas de nós de memória. Ferramentas como numactl e chamadas de sistema da biblioteca libnuma de Andi Kleen eliminam a migração estocástica de processos.
Mover uma thread ativa entre nós NUMA descarta a localidade temporal das caches L1, L2 e L3 locais.
Para evitar essa migração indesejada, engenheiros utilizam a chamada de sistema sched_setaffinity ou pthread_setaffinity_np.
No gerenciamento de memória, o kernel disponibiliza políticas distintas via chamada mbind:
- MPOL_BIND: Restringe as alocações estritamente aos nós especificados, falhando se a memória local esgotar.
- MPOL_PREFERRED: Prioriza a alocação no nó local, recorrendo a nós remotos apenas sob pressão severa de RAM.
- MPOL_INTERLEAVE: Alterna páginas de memória em round-robin entre múltiplos nós NUMA para maximizar a largura de banda agregada.
- MPOL_LOCAL: Aloca páginas no nó da thread que disparou o acesso, mantendo a regra natural de first-touch.
Aplicações orientadas a ultra baixa latência utilizam o comando numactl para amarrar tanto o conjunto de CPUs quanto a memória:
# Executa o binário fixado nos núcleos 0 a 15 com memória estritamente no nó 0
numactl --cpunodebind=0 --membind=0 ./motor_alta_frequencia --config=prod.json
# Inspeciona as distâncias relativas entre nós NUMA na tabela ACPI SLIT
numactl --hardware
A matriz de distâncias exposta pelo hardware via ACPI SLIT (System Locality Information Table) revela os custos relativos normalizados. O valor 10 representa o nó local, enquanto nós vizinhos apresentam distâncias típicas de 16, 24 ou 32.
Por que runtimes com work-stealing global colapsam em nós NUMA múltiplos
Mecanismos globais de roubo de trabalho (work-stealing) transferem tarefas entre núcleos sem considerar a topologia física de caches e nós. Quando um núcleo ocioso rouba trabalho de outro nó NUMA, ele é forçado a processar dados distantes e invalida as caches do proprietário original.
Runtimes assíncronos contemporâneos, como o escalonador padrão de Tokio em Rust ou o runtime da linguagem Go, utilizam filas de tarefas locais por thread de trabalho.
Quando a fila local de um trabalhador esvazia, ele tenta furtar tarefas das filas de outros trabalhadores.
Se essa operação de roubo cruza as fronteiras do nó NUMA, duas patologias graves acontecem simultaneamente:
Em primeiro lugar, a tarefa roubada referencia estruturas e ponteiros que continuam residentes na DRAM do nó de origem. O trabalhador executa o processamento sob constante penalidade de acesso remoto.
Em segundo lugar, a manipulação de ponteiros atômicos na fila de trabalho remota dispara transmissões de invalidação de linha de cache (false sharing em barramento inter-socket).
Para cargas com requisitos de cauda p99 rígidos, a solução arquitetural é migrar do modelo de work-stealing global para o modelo Thread-Per-Core (compartilhado-zero).
Frameworks como Seastar Project e Glommio em Rust criam uma instância isolada do runtime por núcleo físico.
Nesse modelo, threads nunca migram e tarefas nunca são roubadas entre núcleos distintos.
A comunicação entre núcleos diferentes ocorre exclusivamente por meio de filas circulares Single-Producer Single-Consumer (SPSC) lock-free, com memória pré-alocada e dimensionada.
MODELO WORK-STEALING GLOBAL (CONTENÇÃO CROSS-NUMA):
[ Núcleo 0 (Nó 0) ] ----> Rouba Tarefa ----> [ Fila do Núcleo 32 (Nó 1) ]
| |
+=========== Barramento UPI / Infinity Fabric =====+
(Penalidade de 200 ns por acesso)
MODELO THREAD-PER-CORE (ISOLAMENTO COMPLETO):
[ Núcleo 0 (Nó 0) ] <---> Fila SPSC Ponto a Ponto <---> [ Núcleo 32 (Nó 1) ]
(DRAM Local 0) (DRAM Local 1)

Como os alocadores de memória modernos lidam com múltiplos domínios de nós
Alocadores de alta performance evitam travas globais particionando o gerenciamento do heap em arenas dedicadas por CPU ou por nó NUMA. Soluções como jemalloc, mimalloc e TCMalloc eliminam a disputa de memória entre threads em nós diferentes.
Em sistemas multiprocessados, o alocador malloc tradicional da biblioteca glibc tornava-se um gargalo devastador ao exigir sincronização global para cada alocação.
O jemalloc, amplamente adotado em infraestruturas de missão crítica, resolveu esse impasse associando arenas independentes a cada CPU lógica.
Quando uma thread solicita memória, o alocador roteia a requisição para a arena correspondente ao nó NUMA atual da thread.
Pequenos objetos são atendidos a partir de caches locais de thread (thread caches ou tcache), sem qualquer instrução de trava atômica.
Alocadores modernos como o mimalloc da Microsoft e o TCMalloc da Google avançaram essa técnica utilizando Restartable Sequences (rseq).
O recurso rseq do kernel Linux permite que operações de manipulação de pilha local da CPU sejam executadas sem locks atômicos caros. Se o escalonador interrompe a thread no meio da sequência, o kernel reinicia a operação transparentemente.
| Alocador de Memória | Estratégia NUMA e Concorrência | Mecanismo de Cache Local | Vantagem Principal em Produção |
|---|---|---|---|
| jemalloc | Arenas mapeadas por nó NUMA e afinidade de CPU | tcache por thread com reciclagem em lotes | Fragmentação mínima em heaps de longa duração |
| mimalloc | Páginas livres particionadas com tamanhos fixos | Caches de bloco locais e alinhamento de 64 bytes | Desempenho extremo em alocações de vida curta |
| TCMalloc | Gerenciamento central com caches por CPU | ThreadCache e buffers locais baseados em rseq | Baixíssimo overhead em contagens massivas de threads |
| Glibc malloc | Arenas dinâmicas limitadas a 8× contagem de núcleos | Pools genéricos sem garantia estrita de nó | Alto risco de false sharing e contenção de travas |
Como o MaxVision Code estrutura o isolamento NUMA em pipelines de agentes
No ecossistema do MaxVision Code, o processamento de código em tempo real e a execução de agentes de IA exigem segregação estrita de nós para eliminar jitter. A infraestrutura particiona nós de hardware para isolar operações de rede e tarefas de compilação intensiva.
Em um servidor de 128 núcleos físicos com topologia dual-socket, a arquitetura divide a carga de trabalho em dois domínios isolados:
Primeiro, o Nó NUMA 0 (núcleos 0 a 31) fica dedicado à ingestão de rede e mensageria. Conectado diretamente à interface 100 GbE via barramento PCIe local, opera com numa_alloc_onnode para manter buffers de pacotes na DRAM local.
Segundo, o Nó NUMA 1 (núcleos 32 a 63) assume os motores de execução e compilação de agentes. Executa parsers Tree-Sitter, compilações com rustc e inferência de modelos locais, totalmente livre das interrupções de hardware da rede.
A comunicação entre a camada de ingestão no Nó 0 e os agentes no Nó 1 ocorre por meio de ring buffers circulares lock-free.
Os cursores de leitura e escrita recebem espaçamento de 128 bytes (#[repr(align(128))] em Rust) para cobrir linhas de cache adjacentes e evitar pré-carregamento especulativo cruzado.
Com esse particionamento de topologia, o tempo médio de resposta do pipeline no MaxVision Code despencou de 1,8 ms para 142 microssegundos.
O percentil de cauda p99.9 caiu de 34 ms para 420 microssegundos, eliminando completamente as pausas provocadas por saturação no barramento de interconexão.
Checklist de engenharia para produção em arquiteturas NUMA
A sustentação de cargas de alta vazão exige auditoria sistemática das configurações de hardware, kernel e código de aplicação. O checklist a seguir resume as intervenções obrigatórias para garantir simpatia mecânica em servidores de produção.
1. Configuração de Kernel e Inicialização
- Configure isolamento de núcleos sensíveis utilizando parâmetros
isolcpus=domain,managed_irqenohz_fullno GRUB. - Desloque o tratamento de interrupções de hardware da rede e do armazenamento para núcleos utilitários via
smp_affinity. - Monitore e avalie o impacto do balanceador automático do kernel (
sysctl kernel.numa_balancing=0para desativar ruído de migração quando o particionamento for estrito).
2. Gestão de Memória e Alocação
- Garanta que estruturas compartilhadas de grande porte sejam inicializadas em paralelo (parallel first-touch) por todas as threads de trabalho.
- Utilize páginas gigantes transparentes (
THP) ou alocações dedicadas emhugetlbfs(páginas de 2 MB ou 1 GB) para reduzir faltas na tabela TLB (Translation Lookaside Buffer). - Adote alocadores modernos configurados para respeitar topologia local (
jemalloccom arenas particionadas oumimalloc).
3. Código e Arquitetura de Software
- Prefira o modelo Thread-Per-Core compartilhado-zero sobre work-stealing indiscriminado em tarefas de latência crítica.
- Aplique espaçamento explícito de linha de cache (64 ou 128 bytes) em estruturas de dados compartilhadas entre nós para eliminar false sharing.
- Audite regularmente métricas de hardware com contadores PMU para verificar a localidade de dados em execução real.
Fontes e referências canônicas
- Drepper, Ulrich. "What Every Programmer Should Know About Memory." Red Hat / LWN.net, 2007. URL:
https://lwn.net/Articles/72228/. Análise seminal sobre hierarquia de caches, microarquitetura de DRAM e topologia NUMA. - The Linux Kernel Development Community. "NUMA Memory Policy." Linux Kernel Documentation (admin-guide/mm/numa_memory_policy.rst). URL:
https://www.kernel.org/doc/html/latest/admin-guide/mm/numa_memory_policy.html. Especificação oficial das políticas de alocação de memória e chamadasmbind. - Intel Corporation. "Intel 64 and IA-32 Architectures Optimization Reference Manual: Volume 1." Intel Developer Zone, 2024–2026. Documentação técnica cobrindo topologia Ultra Path Interconnect (UPI), Sub-NUMA Clustering (SNC) e protocolos de coerência.
- Advanced Micro Devices (AMD). "AMD EPYC 9004 Series Processor Architecture: High Performance Computing and Enterprise Solutions." AMD Developer Central, 2023–2025. Especificações de Infinity Fabric, Core Complex Dies (CCDs) e arquitetura de controladores de memória descentralizados.
- Kleen, Andi. "An NUMA API for Linux." Novell/SUSE Whitepaper e documentação canônica da biblioteca
libnuma. URL:https://man7.org/linux/man-pages/man3/numa.3.html. - Kivity, Avi; Laor, Dor et al. "Seastar: High Performance Server-Side Application Framework." ScyllaDB Project. URL:
https://seastar.io/. Documentação e código-fonte da arquitetura de referência Thread-Per-Core e eliminação de travas. - Evans, Jason. "jemalloc: Scalable Concurrent Memory Allocation." Meta / jemalloc Project. URL:
https://jemalloc.net/. Arquitetura de particionamento de arenas por nós de memória e mitigação de contenção entre threads. - Microsoft Corporation. "mimalloc: A compact general purpose allocator with excellent performance." Microsoft Research. URL:
https://github.com/microsoft/mimalloc. Implementação de blocos livres, caches de alinhamento e integração com sequências reiniciáveis (rseq).