NUMA 심화 (Non-Uniform Memory Access)

NUMA 하드웨어 토폴로지, ACPI 테이블 파싱, 커널 자료구조, 메모리 정책, Automatic NUMA Balancing, NUMA-aware 스케줄링, CXL 확장까지 — 비균일 메모리 접근 아키텍처를 소스 코드 수준에서 분석합니다.

관련 표준: ACPI 6.5 (SRAT/SLIT/HMAT 테이블), JEDEC DDR5 (메모리 채널 구조), CXL 3.1 (캐시 일관성 인터커넥트) — NUMA 토폴로지 탐색과 메모리 접근에 관련된 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
참고: NUMA의 기초 개념과 pg_data_t 개요는 메모리 관리 — NUMA 섹션을, Hugepage와 NUMA 연동은 메모리 심화 — Hugepage와 NUMA 섹션을 참조하세요.
전제 조건: 메모리 관리(Node, Zone 개념)와 CPU 토폴로지(소켓, 코어 계층)를 먼저 읽으세요.
일상 비유: NUMA는 여러 도서관이 연결된 대학 캠퍼스와 같습니다. 각 건물(Node)에는 자체 서가(로컬 메모리)가 있고, 같은 건물 내에서 책을 찾으면 빠르지만(로컬 접근), 다른 건물의 책을 빌리려면 시간이 더 걸립니다(원격 접근). NUMA 정책은 "어느 건물 서가에서 책을 꺼낼지" 결정하는 규칙입니다.

핵심 요약

  • NUMA — 메모리 접근 시간이 CPU와 메모리의 물리적 위치에 따라 달라지는 아키텍처입니다.
  • Node — CPU 소켓과 로컬 메모리를 묶은 단위. 커널에서 pglist_data 구조체로 표현됩니다.
  • SRAT/SLIT — ACPI 테이블로 NUMA 토폴로지(노드 구성)와 거리(접근 지연)를 커널에 전달합니다.
  • 메모리 정책mbind()/set_mempolicy()로 프로세스의 메모리 할당 노드를 제어합니다.
  • NUMA Balancing — 커널이 자동으로 페이지를 자주 접근하는 CPU 노드로 마이그레이션합니다.

단계별 이해

  1. 토폴로지 확인numactl --hardware로 시스템의 NUMA 노드 수, 각 노드의 CPU와 메모리 크기를 확인합니다.

    lscpu에서도 NUMA 노드 정보를 볼 수 있습니다.

  2. 로컬 vs 원격 접근 — 같은 노드의 메모리 접근은 빠르고, 다른 노드 접근은 느립니다.

    numastat으로 노드별 할당 통계(로컬 hit/miss)를 모니터링할 수 있습니다.

  3. 정책 적용numactl --cpubind=0 --membind=0 ./app으로 특정 노드에 CPU와 메모리를 바인딩합니다.

    데이터베이스 같은 지연 민감 워크로드에서 큰 성능 차이를 만듭니다.

  4. 커널 자동 밸런싱/proc/sys/kernel/numa_balancing으로 자동 마이그레이션을 활성화/비활성화합니다.

    커널이 주기적으로 페이지 접근 패턴을 분석하여 최적 노드로 이동시킵니다.

1. NUMA 하드웨어 토폴로지

NUMA(Non-Uniform Memory Access) 시스템에서 각 CPU 소켓은 자신에게 직접 연결된 로컬 메모리를 가지며, 다른 소켓의 메모리에 접근할 때는 인터커넥트(QPI, UPI, Infinity Fabric 등)를 경유합니다. 이로 인해 메모리 접근 지연 시간(latency)과 대역폭(bandwidth)이 위치에 따라 달라집니다.

NUMA Node 0 CPU 0-7 L3 Cache Local Memory 64 GB DDR5 NUMA Node 1 CPU 8-15 L3 Cache Local Memory 64 GB DDR5 UPI / IF Local: ~80ns | Remote: ~140ns Local: ~80ns | Remote: ~140ns NUMA Distance Matrix Node 0→0: 10 | Node 0→1: 21 Node 1→0: 21 | Node 1→1: 10
2-소켓 NUMA 토폴로지 — 로컬/리모트 메모리 접근 지연 시간 차이

NUMA Ratio와 영향

시스템 유형인터커넥트로컬 지연리모트 지연NUMA Ratio
Intel 2S (Xeon Scalable)UPI 2.0/3.0~80ns~130-150ns1.6-1.9x
Intel 4S/8SUPI (멀티홉)~80ns~170-300ns2.1-3.7x
AMD EPYC (2S)Infinity Fabric~90ns~140-160ns1.5-1.8x
AMD EPYC (NPS4)IF (소켓 내)~85ns~110-130ns1.3-1.5x
ARM64 서버CCIX/CXL~100ns~200-350ns2.0-3.5x
CXL 메모리 확장CXL 2.0~80ns~170-250ns2.1-3.1x
AMD NPS (Nodes Per Socket): AMD EPYC 프로세서는 NPS 설정으로 단일 소켓 내부를 여러 NUMA 노드로 분할합니다. NPS1(1노드), NPS2(2노드), NPS4(4노드) 모드를 지원합니다. NPS4에서는 소켓 당 4개의 CCD 그룹이 각각 독립 NUMA 노드가 되어, 로컬 메모리 접근 범위는 줄지만 지연 시간 편차가 감소합니다.
CPU 토폴로지 심화: AMD CCX/CCD/IOD 칩렛 구조, Intel Tile/Hybrid 아키텍처, ARM DynamIQ, Infinity Fabric, 스케줄링 도메인 계층에 대한 종합적인 내용은 CPU 토폴로지 페이지를 참조하세요.

2. ACPI를 통한 NUMA 토폴로지 검색

커널은 부팅 시 ACPI 테이블을 파싱하여 NUMA 토폴로지를 구성합니다. 핵심 테이블은 SRAT(Static Resource Affinity Table)와 SLIT(System Locality Information Table)입니다.

SRAT (Static Resource Affinity Table)

/*
 * SRAT는 CPU와 메모리가 어떤 NUMA 노드에 속하는지 정의합니다.
 *
 * SRAT 하위 구조:
 * - Processor Local APIC Affinity: CPU(APIC ID) → Node 매핑
 * - Memory Affinity: 메모리 범위 → Node 매핑
 * - Processor Local x2APIC Affinity: x2APIC CPU → Node 매핑
 * - GICC Affinity: ARM64 CPU → Node 매핑
 * - Generic Initiator Affinity: CXL 장치 등 → Node 매핑
 */

/* arch/x86/kernel/acpi/srat.c */
static int __init
acpi_parse_processor_affinity(union acpi_subtable_headers *header,
                              const unsigned long end)
{
    struct acpi_srat_cpu_affinity *p =
        (struct acpi_srat_cpu_affinity *)header;

    int pxm = p->proximity_domain_lo |
              (p->proximity_domain_hi[0] << 8) |
              (p->proximity_domain_hi[1] << 16) |
              (p->proximity_domain_hi[2] << 24);

    /* proximity domain → NUMA node 매핑 등록 */
    set_apicid_to_node(p->apic_id, pxm_to_node(pxm));
    return 0;
}

static int __init
acpi_parse_memory_affinity(union acpi_subtable_headers *header,
                           const unsigned long end)
{
    struct acpi_srat_mem_affinity *ma =
        (struct acpi_srat_mem_affinity *)header;

    u64 start = ma->base_address;
    u64 length = ma->length;
    int node = pxm_to_node(ma->proximity_domain);

    /* 메모리 범위를 노드에 등록 */
    numa_add_memblk(node, start, start + length);
    return 0;
}

SLIT (System Locality Information Table)

/*
 * SLIT는 노드 간 상대적 거리를 N×N 매트릭스로 정의합니다.
 * 거리 10 = 자기 자신 (로컬), 값이 클수록 먼 노드
 *
 * 예시: 4-노드 시스템 SLIT 매트릭스
 *       Node0  Node1  Node2  Node3
 * Node0:  10     21     31     41
 * Node1:  21     10     21     31
 * Node2:  31     21     10     21
 * Node3:  41     31     21     10
 */

/* drivers/acpi/numa/srat.c */
void __init acpi_numa_slit_init(struct acpi_table_slit *slit)
{
    int i, j;
    for (i = 0; i < slit->locality_count; i++)
        for (j = 0; j < slit->locality_count; j++)
            numa_set_distance(
                pxm_to_node(i),
                pxm_to_node(j),
                slit->entry[i * slit->locality_count + j]);
}

토폴로지 확인 명령

# NUMA 노드 목록
$ ls /sys/devices/system/node/
node0  node1

# 노드별 CPU 매핑
$ cat /sys/devices/system/node/node0/cpulist
0-7,16-23
$ cat /sys/devices/system/node/node1/cpulist
8-15,24-31

# 노드 간 거리 매트릭스
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 65366 MB
node 0 free: 48230 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 65536 MB
node 1 free: 51200 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

# SRAT 정보 (dmesg)
$ dmesg | grep -i srat
ACPI: SRAT: Node 0 PXM 0 [mem 0x00000000-0x0fffffff]
ACPI: SRAT: Node 0 PXM 0 [mem 0x100000000-0xfffffffff]
ACPI: SRAT: Node 1 PXM 1 [mem 0x1000000000-0x1fffffffff]

# lstopo (hwloc)로 시각적 토폴로지 확인
$ lstopo --of txt
Machine (128GB total)
  NUMANode L#0 (P#0 64GB)
    Package L#0
      L3 L#0 (30MB)
        L2 L#0 (256KB) + L1d L#0 (32KB) + L1i L#0 (32KB) + Core L#0
          PU L#0 (P#0)
          PU L#1 (P#16)
        ...
  NUMANode L#1 (P#1 64GB)
    ...

3. 커널 자료구조: pglist_data

각 NUMA 노드는 struct pglist_data (별칭 pg_data_t)로 표현됩니다. 이 구조체는 노드의 메모리 존, 페이지 프레임, 통계 정보를 모두 관리합니다.

/* include/linux/mmzone.h (주요 필드 발췌) */
typedef struct pglist_data {
    /* ---- 존 정보 ---- */
    struct zone node_zones[MAX_NR_ZONES];    /* 노드 내 존 배열 */
    struct zonelist node_zonelists[MAX_ZONELISTS]; /* 할당 폴백 순서 */
    int nr_zones;                             /* 활성 존 수 */

    /* ---- 페이지 프레임 ---- */
    struct page *node_mem_map;                 /* 노드의 struct page 배열 */
    unsigned long node_start_pfn;              /* 시작 페이지 프레임 번호 */
    unsigned long node_present_pages;           /* 실제 존재하는 페이지 수 */
    unsigned long node_spanned_pages;           /* 시작~끝 범위 (hole 포함) */
    int node_id;                               /* 이 노드의 번호 */

    /* ---- 페이지 회수 (reclaim) ---- */
    wait_queue_head_t kswapd_wait;             /* kswapd 대기 큐 */
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd;                /* 이 노드의 kswapd 스레드 */
    int kswapd_order;
    enum zone_type kswapd_highest_zoneidx;

    /* ---- LRU 리스트 (페이지 에이징) ---- */
    struct lruvec __lruvec;                    /* 노드 단위 LRU 벡터 */

    /* ---- 통계 ---- */
    unsigned long totalreserve_pages;
    struct per_cpu_nodestat __percpu *per_cpu_nodestats;

    /* ---- Compaction ---- */
    unsigned long compact_cached_free_pfn;
    unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];

    /* ---- NUMA Balancing ---- */
    spinlock_t numabalancing_migrate_lock;
    unsigned long numabalancing_migrate_nr_pages;
    unsigned long numabalancing_migrate_next_window;
} pg_data_t;

/* 전역 노드 배열 */
extern struct pglist_data *node_data[];
#define NODE_DATA(nid)  (node_data[nid])

Zonelist — 할당 폴백 순서

/*
 * 메모리 할당 시 존리스트(zonelist)를 따라 폴백합니다.
 *
 * Node 0의 zonelist[ZONELIST_FALLBACK]:
 *   Node0:ZONE_NORMAL → Node0:ZONE_DMA32 → Node0:ZONE_DMA
 *   → Node1:ZONE_NORMAL → Node1:ZONE_DMA32 → Node1:ZONE_DMA
 *
 * 순서: 로컬 노드 우선 → 거리가 가까운 노드 순
 * SLIT 거리 기반으로 정렬됨
 */

struct zonelist {
    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

struct zoneref {
    struct zone *zone;       /* 존 포인터 */
    int zone_idx;             /* 존 인덱스 */
};

/* zonelist 탐색 매크로 */
for_each_zone_zonelist(zone, z, zonelist, highest_zoneidx) {
    /* 로컬 노드부터 리모트 노드까지 순서대로 시도 */
    page = rmqueue(zone, order, gfp_mask);
    if (page)
        return page;
}

노드 통계 확인

# 노드별 메모리 통계 (/sys/devices/system/node/node*/meminfo)
$ cat /sys/devices/system/node/node0/meminfo
Node 0 MemTotal:       65536000 kB
Node 0 MemFree:        48230000 kB
Node 0 MemUsed:        17306000 kB
Node 0 Active:         8120000 kB
Node 0 Inactive:       6450000 kB
Node 0 AnonPages:      5230000 kB
Node 0 FilePages:      9340000 kB
Node 0 Slab:           1530000 kB
Node 0 SReclaimable:   1200000 kB
...

# 노드별 존 정보
$ cat /proc/zoneinfo | grep -A3 "Node 0"
Node 0, zone   Normal
  pages free     12057500
        min      16384
        low      20480

# numastat — 노드별 NUMA 적중/미스 통계
$ numastat
                           node0           node1
numa_hit               142857391       98234567
numa_miss                1234567        2345678
numa_foreign             2345678        1234567
interleave_hit           3456789        3456789
local_node             139400824       95888889
other_node               4690134        4690356

4. NUMA 메모리 정책

Linux 커널은 프로세스와 메모리 영역별로 NUMA 메모리 할당 정책을 지정할 수 있습니다. 이는 set_mempolicy(), mbind() 시스템 콜과 numactl 도구로 제어합니다.

정책 유형

정책상수동작용도
DefaultMPOL_DEFAULT프로세스가 실행 중인 CPU의 로컬 노드에서 할당대부분의 일반 워크로드
BindMPOL_BIND지정된 노드 집합에서만 할당 (실패 시 OOM)메모리 격리, 전용 노드
PreferredMPOL_PREFERRED선호 노드에서 우선 할당, 실패 시 다른 노드 폴백소프트 바인딩
Preferred ManyMPOL_PREFERRED_MANY여러 선호 노드 지정 가능, 순서대로 시도유연한 선호 (v5.15+)
InterleaveMPOL_INTERLEAVE지정된 노드들에 라운드-로빈으로 페이지 분산대역폭 극대화, 해시 테이블
LocalMPOL_LOCAL항상 현재 CPU의 로컬 노드에서 할당마이그레이션 후에도 로컬 유지
Weighted InterleaveMPOL_WEIGHTED_INTERLEAVE노드별 대역폭 가중치로 분산CXL 이종 메모리 (v6.9+)

시스템 콜

#include <numaif.h>

/* 프로세스 전체 NUMA 정책 설정 */
long set_mempolicy(
    int mode,                /* MPOL_DEFAULT, MPOL_BIND, ... */
    const unsigned long *nodemask,  /* 대상 노드 비트마스크 */
    unsigned long maxnode   /* nodemask 비트 수 */
);

/* 특정 메모리 영역의 NUMA 정책 설정 */
long mbind(
    void *addr,              /* 시작 주소 (페이지 정렬) */
    unsigned long len,       /* 길이 */
    int mode,                /* MPOL_BIND, MPOL_INTERLEAVE, ... */
    const unsigned long *nodemask,
    unsigned long maxnode,
    unsigned int flags       /* MPOL_MF_MOVE, MPOL_MF_STRICT, ... */
);

/* 페이지의 현재 노드 위치 조회 */
long get_mempolicy(
    int *policy,
    unsigned long *nodemask,
    unsigned long maxnode,
    void *addr,
    unsigned long flags      /* MPOL_F_NODE, MPOL_F_ADDR */
);

/* 페이지를 다른 노드로 마이그레이션 */
long migrate_pages(
    pid_t pid,
    unsigned long maxnode,
    const unsigned long *old_nodes,  /* 원본 노드 */
    const unsigned long *new_nodes   /* 대상 노드 */
);

/* 개별 페이지 단위 마이그레이션 */
long move_pages(
    pid_t pid,
    unsigned long count,
    void **pages,             /* 페이지 주소 배열 */
    const int *nodes,        /* 대상 노드 배열 (NULL이면 조회) */
    int *status,              /* 결과/현재 노드 */
    int flags
);

numactl 사용

# 특정 노드에서만 메모리 할당 (bind)
$ numactl --membind=0 ./my_application

# 특정 CPU에서 실행 + 해당 노드의 메모리 사용
$ numactl --cpunodebind=0 --membind=0 ./my_application

# 인터리브 모드 (모든 노드에 분산)
$ numactl --interleave=all ./hash_table_server

# 선호 노드 지정 (폴백 허용)
$ numactl --preferred=1 ./my_application

# 현재 실행 중인 프로세스의 NUMA 매핑 확인
$ numastat -p 
Per-node process memory usage (in MBs) for PID 
                          Node 0      Node 1       Total
                 --------- --------- -----------
Huge                  0.00      0.00        0.00
Heap                256.50     12.30      268.80
Stack                 0.12      0.00        0.12
Private            1024.00     48.00     1072.00
...

# 프로세스의 NUMA 메모리 맵 (/proc/PID/numa_maps)
$ cat /proc/self/numa_maps
00400000 default file=/usr/bin/cat mapped=10 N0=10
7f8a1000 default anon=3 dirty=3 N0=2 N1=1
7ffd2000 default stack anon=2 dirty=2 N0=2

커널 내부 NUMA 할당 API

/* NUMA-aware 커널 메모리 할당 */

/* 특정 노드에서 페이지 할당 */
struct page *alloc_pages_node(int nid, gfp_t gfp, unsigned int order);

/* 현재 CPU의 로컬 노드에서 할당 */
struct page *alloc_pages(gfp_t gfp, unsigned int order);

/* 특정 노드에서 slab 할당 */
void *kmalloc_node(size_t size, gfp_t flags, int node);
void *kzalloc_node(size_t size, gfp_t flags, int node);
void *kvmalloc_node(size_t size, gfp_t flags, int node);

/* kmem_cache에서 특정 노드 할당 */
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t flags, int node);

/* GFP 플래그로 NUMA 제어 */
__GFP_THISNODE   /* 지정된 노드에서만 할당 (폴백 금지) */

/* 현재 CPU의 NUMA 노드 번호 */
int nid = numa_node_id();         /* 현재 CPU의 노드 */
int nid = cpu_to_node(cpu);       /* 특정 CPU의 노드 */
int nid = page_to_nid(page);      /* 페이지가 속한 노드 */

/* 디바이스의 NUMA 노드 (PCIe 장치 등) */
int nid = dev_to_node(dev);        /* 장치에 가장 가까운 노드 */

/* 예시: NIC의 로컬 노드에 sk_buff 할당 */
int nid = dev_to_node(&netdev->dev);
skb = __alloc_skb(size, GFP_ATOMIC, 0, nid);

5. Automatic NUMA Balancing

Automatic NUMA Balancing은 커널이 자동으로 프로세스의 메모리를 적절한 NUMA 노드로 마이그레이션하는 메커니즘입니다. 사용자 공간의 개입 없이 NUMA 지역성을 최적화합니다.

동작 원리

/*
 * Automatic NUMA Balancing 동작 흐름:
 *
 * 1. 스캔 (Scan):
 *    - task_tick_numa()가 주기적으로 호출됨
 *    - 프로세스의 VMA를 스캔하며 PTE의 접근 비트를 제거하고
 *      PROT_NONE으로 변경 ("NUMA hinting fault" 설정)
 *
 * 2. 폴트 (Fault):
 *    - 프로세스가 PROT_NONE 페이지에 접근하면 page fault 발생
 *    - do_numa_page()에서 어떤 CPU(노드)가 접근했는지 기록
 *
 * 3. 마이그레이션 판단 (Decision):
 *    - 페이지에 접근하는 주요 CPU의 노드와 페이지의 현재 노드 비교
 *    - 다른 노드라면 마이그레이션 후보로 등록
 *    - 스케줄러도 태스크를 페이지가 있는 노드로 옮길지 판단
 *
 * 4. 마이그레이션 (Migration):
 *    - migrate_misplaced_page()로 페이지를 적절한 노드로 이동
 *    - 또는 스케줄러가 태스크를 페이지가 있는 노드로 이동
 */

NUMA Hinting Fault

/* mm/memory.c — NUMA hinting fault 처리 */
static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page;
    int page_nid, target_nid, last_cpupid;
    bool migrated;

    /* 현재 PTE 복원 (PROT_NONE → 원래 권한) */
    pte = pte_modify(old_pte, vma->vm_page_prot);

    page = vm_normal_page(vma, vmf->address, pte);
    page_nid = page_to_nid(page);        /* 페이지의 현재 노드 */
    target_nid = numa_migrate_prep(page, vma, vmf->address,
                                    page_nid, &flags);

    if (target_nid == NUMA_NO_NODE) {
        /* 마이그레이션 불필요: 이미 최적 위치 */
        put_page(page);
        goto out;
    }

    /* 페이지를 target_nid로 마이그레이션 시도 */
    migrated = migrate_misplaced_page(page, vma, target_nid);
    if (migrated)
        page_nid = target_nid;

out:
    /* NUMA 폴트 통계 업데이트 */
    if (page_nid != NUMA_NO_NODE)
        task_numa_fault(last_cpupid, page_nid,
                       1, flags);
    return 0;
}

튜닝 파라미터

# Automatic NUMA Balancing 활성/비활성
$ sysctl kernel.numa_balancing
kernel.numa_balancing = 1                   # 1=활성, 0=비활성

# 스캔 주기 (ms) — 폴트가 없으면 점점 늘어남
$ sysctl kernel.numa_balancing_scan_delay_ms
kernel.numa_balancing_scan_delay_ms = 1000  # 초기 스캔 지연

$ sysctl kernel.numa_balancing_scan_period_min_ms
kernel.numa_balancing_scan_period_min_ms = 1000  # 최소 스캔 주기

$ sysctl kernel.numa_balancing_scan_period_max_ms
kernel.numa_balancing_scan_period_max_ms = 60000 # 최대 스캔 주기

# 한 번에 스캔할 페이지 수
$ sysctl kernel.numa_balancing_scan_size_mb
kernel.numa_balancing_scan_size_mb = 256    # MB 단위

# 프로세스별 NUMA 폴트 통계
$ cat /proc//sched | grep numa
numa_pages_migrated                          :              12345
numa_preferred_nid                           :              0
total_numa_faults                            :              67890
NUMA Balancing 비활성화가 좋은 경우: 1) 이미 numactl --membind로 메모리를 명시적으로 바인딩한 경우, 2) 대규모 인메모리 DB(Redis, memcached)에서 마이그레이션 오버헤드가 크면 성능 저하, 3) 실시간(RT) 워크로드에서 NUMA fault의 지연 시간 편차가 허용 불가, 4) KVM 게스트에서 호스트의 NUMA balancing과 충돌할 수 있음.

6. NUMA-aware 스케줄링

CFS 스케줄러는 NUMA 토폴로지를 인식하여 태스크를 배치합니다. 스케줄링 도메인(sched_domain) 계층이 NUMA 거리를 반영합니다.

스케줄링 도메인 계층

/*
 * 2-소켓 서버의 스케줄링 도메인 계층:
 *
 * ┌────────────────────────────────────────────┐
 * │              SD_NUMA (전체 시스템)            │
 * │  flags: SD_NUMA | SD_SERIALIZE             │
 * │  ┌──────────────────┐  ┌──────────────────┐│
 * │  │   SD_MC (Node 0) │  │   SD_MC (Node 1) ││
 * │  │   flags: SD_SHARE│  │   flags: SD_SHARE││
 * │  │   _PKG_RESOURCES │  │   _PKG_RESOURCES ││
 * │  │ ┌──────────────┐ │  │ ┌──────────────┐ ││
 * │  │ │ SD_SMT (Core)│ │  │ │ SD_SMT (Core)│ ││
 * │  │ │ HT 0 | HT 1 │ │  │ │ HT 0 | HT 1 │ ││
 * │  │ └──────────────┘ │  │ └──────────────┘ ││
 * │  └──────────────────┘  └──────────────────┘│
 * └────────────────────────────────────────────┘
 *
 * 로드 밸런싱 시 SMT→MC→NUMA 순으로 상위 도메인 탐색
 * NUMA 도메인에서의 마이그레이션 비용이 가장 높음
 */

# 스케줄링 도메인 확인
$ ls /proc/sys/kernel/sched_domain/cpu0/
domain0  domain1  domain2

$ cat /proc/sys/kernel/sched_domain/cpu0/domain0/name
SMT
$ cat /proc/sys/kernel/sched_domain/cpu0/domain1/name
MC
$ cat /proc/sys/kernel/sched_domain/cpu0/domain2/name
NUMA
💡

스케줄링 도메인의 전체 계층 구조(SMT → MC → CL → DIE → NUMA), SD 플래그 상세, EAS(Energy Aware Scheduling) 등은 CPU 토폴로지 — 스케줄링 도메인에서 상세히 다룹니다.

태스크 선호 노드

/* kernel/sched/fair.c — NUMA 폴트 기반 선호 노드 결정 */

/*
 * task_struct에서 NUMA 관련 필드:
 */
struct task_struct {
    ...
    int                          numa_preferred_nid;  /* 선호 노드 */
    unsigned long                numa_scan_seq;       /* 스캔 시퀀스 */
    unsigned long                numa_scan_period;    /* 현재 스캔 주기 */
    unsigned long                numa_scan_offset;    /* 스캔 오프셋 */
    struct numa_group            *numa_group;         /* NUMA 그룹 */
    unsigned long                *numa_faults;        /* 노드별 폴트 카운터 */
    unsigned long                total_numa_faults;
    unsigned long                numa_pages_migrated;
    ...
};

/*
 * task_numa_fault()가 폴트 이력을 분석하여
 * 가장 많은 메모리 접근이 발생하는 노드를
 * numa_preferred_nid로 설정합니다.
 *
 * CFS의 wake_affine()과 find_idlest_group()은
 * numa_preferred_nid를 참고하여 태스크 배치를 결정합니다.
 */

NUMA 그룹

/*
 * NUMA 그룹은 메모리를 공유하는 태스크들을 묶어서
 * 함께 같은 노드로 마이그레이션합니다.
 *
 * 예: 멀티스레드 애플리케이션의 스레드들이
 *     같은 공유 메모리를 접근하면 하나의 NUMA 그룹으로 묶임
 *     → 스케줄러가 그룹 전체를 같은 노드에 배치하려 함
 */

struct numa_group {
    refcount_t       refcount;
    spinlock_t       lock;
    int              nr_tasks;       /* 그룹 내 태스크 수 */
    pid_t            gid;            /* 그룹 ID */
    int              active_nodes;   /* 활성 노드 수 */
    struct rcu_head  rcu;
    unsigned long    total_faults;
    unsigned long    max_faults_cpu;
    unsigned long    faults[];       /* 노드별 집계된 폴트 */
};

7. 페이지 마이그레이션

NUMA 페이지 마이그레이션은 페이지를 한 노드에서 다른 노드로 이동시키는 메커니즘입니다. NUMA balancing, migrate_pages() 시스템 콜, 메모리 핫플러그 등에서 사용됩니다.

마이그레이션 흐름

/*
 * 페이지 마이그레이션 단계:
 *
 * 1. 대상 노드에 새 페이지 할당
 * 2. 원본 페이지 잠금 (lock_page)
 * 3. 모든 PTE에서 원본 페이지 언매핑 (try_to_migrate)
 *    - PTE를 migration entry로 교체
 *    - 이 동안 접근하는 프로세스는 대기
 * 4. 페이지 데이터 복사 (migrate_folio / copy_highpage)
 * 5. 새 페이지로 PTE 재매핑 (remove_migration_ptes)
 * 6. 원본 페이지 해제
 */

/* mm/migrate.c — 핵심 마이그레이션 함수 (간략화) */
static int migrate_folio_move(
    free_folio_t put_new_folio,
    unsigned long private,
    struct folio *src,
    struct folio *dst,
    enum migrate_mode mode)
{
    int rc;

    /* 1. 원본 folio 잠금 */
    folio_lock(src);

    /* 2. 모든 매핑에서 PTE 제거 (migration entry 설치) */
    try_to_migrate(src, TTU_BATCH_FLUSH);

    /* 3. 파일시스템/드라이버의 migrate 콜백 호출 */
    rc = move_to_new_folio(dst, src, mode);
    if (rc == MIGRATEPAGE_SUCCESS) {
        /* 4. migration entry를 새 folio의 PTE로 교체 */
        remove_migration_ptes(src, dst, false);
    }

    folio_unlock(src);
    return rc;
}

마이그레이션 도구

# 프로세스의 모든 페이지를 node0에서 node1로 이동
$ migratepages  0 1

# 특정 메모리 영역을 다른 노드로 바인딩 (기존 페이지도 이동)
$ numactl --membind=1 --touch ./app
# 또는 mbind() + MPOL_MF_MOVE 플래그

# 마이그레이션 통계 확인
$ cat /proc/vmstat | grep numa
numa_pte_updates 1234567      # NUMA hinting fault용 PTE 변경 수
numa_huge_pte_updates 12345  # hugepage PTE 변경 수
numa_hint_faults 567890      # NUMA hinting fault 발생 수
numa_hint_faults_local 456789 # 로컬 노드 접근 (이동 불필요)
numa_pages_migrated 98765    # 마이그레이션된 페이지 수
pgmigrate_success 98000      # 성공한 마이그레이션
pgmigrate_fail 765           # 실패한 마이그레이션

8. NUMA-aware 서브시스템

커널의 주요 서브시스템은 NUMA를 인식하여 데이터를 로컬 노드에 배치합니다.

Slab 할당기 (SLUB)

/*
 * SLUB 할당기는 노드별로 독립적인 partial slab 리스트를 유지합니다.
 *
 * struct kmem_cache_node {
 *     spinlock_t list_lock;
 *     unsigned long nr_partial;      // 부분 사용 slab 수
 *     struct list_head partial;      // partial slab 리스트
 * };
 *
 * kmem_cache는 노드별 kmem_cache_node를 배열로 관리:
 * kmem_cache->node[MAX_NUMNODES]
 *
 * 할당 흐름:
 * 1. 현재 CPU의 per-cpu slab에서 시도 (가장 빠름)
 * 2. 현재 노드의 partial 리스트에서 시도
 * 3. 다른 노드의 partial 리스트에서 시도 (cross-node)
 * 4. 새 slab 페이지 할당 (현재 노드 우선)
 */

# 노드별 slab 통계
$ cat /proc/slabinfo  # 전체 통계
$ slabinfo -N 0       # node0의 slab 정보

# slabtop으로 실시간 모니터링
$ slabtop -s c

Per-CPU 변수와 NUMA

/*
 * Per-CPU 변수는 각 CPU의 로컬 NUMA 노드에 할당됩니다.
 * pcpu_alloc()이 cpu_to_node()를 사용하여 적절한 노드에 메모리 배치.
 *
 * 부팅 시 per-cpu 영역 초기화:
 * setup_per_cpu_areas() → pcpu_embed_first_chunk()
 * → 각 CPU에 대해 해당 노드의 메모리에 per-cpu 영역 할당
 */

/* 올바른 NUMA-aware per-CPU 사용 */
DEFINE_PER_CPU(struct my_stats, cpu_stats);

/* 접근 시 preemption 비활성화 필수 (다른 CPU로 이동 방지) */
preempt_disable();
this_cpu_inc(cpu_stats.counter);   /* 로컬 NUMA 노드 접근 보장 */
preempt_enable();

/* NUMA-aware workqueue */
alloc_workqueue("my_wq", WQ_UNBOUND | WQ_NUMA, 0);
/* WQ_NUMA: work item을 제출한 CPU의 NUMA 노드에서 실행 */

네트워크 스택과 NUMA

/* NIC의 NUMA 노드 확인 */
$ cat /sys/class/net/eth0/device/numa_node
0

/* IRQ를 NIC의 로컬 노드 CPU에 바인딩 (최적 성능) */
# NIC가 node0에 있으면 node0의 CPU에 IRQ 할당
$ echo 0-7 > /proc/irq//smp_affinity_list

/* NUMA-aware sk_buff 할당 */
/* 네트워크 드라이버에서 NIC 로컬 노드의 메모리로 sk_buff 할당:
 * dev_alloc_skb() → __netdev_alloc_skb()
 *   → 현재 CPU의 page_frag 캐시 사용
 *   → NAPI 컨텍스트에서는 NIC의 로컬 노드에서 할당
 */

# ethtool로 RX/TX 큐 확인
$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
    RX:     0
    TX:     0
    Other:  0
    Combined:   32
Current hardware settings:
    Combined:   16

# 각 큐의 IRQ가 NIC 로컬 노드에 바인딩되었는지 확인
$ for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo "IRQ $irq: node $(cat /proc/irq/$irq/node)"
done

9. NUMA와 메모리 회수

각 NUMA 노드는 자체 kswapd 스레드를 가지며, 노드별로 독립적으로 메모리 회수(reclaim)가 수행됩니다.

/*
 * NUMA 노드별 kswapd:
 *
 * Node 0: kswapd0 → node0의 존들을 모니터링/회수
 * Node 1: kswapd1 → node1의 존들을 모니터링/회수
 *
 * 각 kswapd는 자기 노드의 워터마크를 기준으로 동작:
 * - pages_free < watermark_low → kswapd 깨어남
 * - pages_free > watermark_high → kswapd 슬립
 *
 * 문제: Node 0이 메모리 부족해도 Node 1에 충분하면
 *       시스템 전체로는 여유가 있지만 Node 0에서 OOM 발생 가능
 */

# 노드별 kswapd 확인
$ ps aux | grep kswapd
root   31  0.0  0.0  0  0 S  ? 0:05 [kswapd0]
root   32  0.0  0.0  0  0 S  ? 0:03 [kswapd1]

# 노드별 워터마크 확인
$ cat /proc/zoneinfo | grep -A5 "Node 0, zone   Normal"
Node 0, zone   Normal
  pages free     12057500
        boost    0
        min      16384
        low      20480
        high     24576

Zone Reclaim Mode

# zone_reclaim_mode — 로컬 노드 메모리 부족 시 동작 제어
$ sysctl vm.zone_reclaim_mode
vm.zone_reclaim_mode = 0

# 비트 플래그:
# 0 (기본): 로컬 노드 부족 시 리모트 노드에서 할당 (대부분의 경우 최적)
# 1 (RECLAIM_ZONE):  로컬 존에서 페이지 회수 시도
# 2 (RECLAIM_WRITE): 더티 페이지 쓰기 후 회수
# 4 (RECLAIM_UNMAP): 매핑된 페이지도 언매핑 후 회수

# zone_reclaim_mode=1이 유리한 경우:
# - NUMA ratio가 매우 큰 시스템 (3x 이상)
# - 메모리 접근 패턴이 극도로 로컬한 워크로드
# - 파일 캐시보다 애플리케이션 데이터가 중요한 경우

# 대부분의 경우 zone_reclaim_mode=0이 권장됨:
# - 리모트 노드 할당 비용 < 페이지 회수 비용
# - 파일 캐시 유지가 전체 성능에 유리

10. CXL과 이종 NUMA

CXL(Compute Express Link)은 CPU에 외부 메모리를 연결하여 NUMA 노드로 노출하는 새로운 인터커넥트 기술입니다. CXL 메모리는 로컬 DRAM보다 지연이 크지만, 용량 확장과 비용 효율성을 제공합니다.

CXL 메모리 토폴로지

/*
 * CXL Type 3 메모리 확장 장치:
 *
 *   CPU Socket 0              CXL Device
 *   ┌──────────────┐          ┌──────────────┐
 *   │  Node 0      │   CXL    │  Node 2      │
 *   │  Local DRAM  │◄────────►│  CXL Memory  │
 *   │  64 GB       │  ~170ns  │  256 GB      │
 *   │  ~80ns       │          │              │
 *   └──────────────┘          └──────────────┘
 *
 *   CPU Socket 1
 *   ┌──────────────┐
 *   │  Node 1      │
 *   │  Local DRAM  │
 *   │  64 GB       │
 *   │  ~80ns       │
 *   └──────────────┘
 *
 * 거리 매트릭스:
 *        Node0  Node1  Node2(CXL)
 * Node0:  10     21     28
 * Node1:  21     10     31
 * Node2:  28     31     10
 */

# CXL 메모리 노드 확인
$ dmesg | grep cxl
cxl_acpi ACPI0017:00: CXL region registered
cxl_mem mem0: CXL Type 3 device, 256 GB

# CXL 노드의 HMAT (Heterogeneous Memory Attribute Table) 정보
$ cat /sys/devices/system/node/node2/access0/initiators/read_latency
170
$ cat /sys/devices/system/node/node2/access0/initiators/read_bandwidth
32000

Weighted Interleave (v6.9+)

/*
 * CXL처럼 노드 간 대역폭이 다른 이종 NUMA 환경에서는
 * 단순 라운드-로빈 인터리브가 비효율적입니다.
 *
 * Weighted Interleave는 노드별 대역폭 비율에 맞춰
 * 페이지를 가중 분산합니다.
 *
 * 예: Node0 (DRAM, 50GB/s) + Node2 (CXL, 16GB/s)
 * 가중치: Node0=3, Node2=1
 * → 4페이지 중 3페이지는 Node0, 1페이지는 Node2에 할당
 */

# 가중치 설정 (sysfs)
$ echo 3 > /sys/kernel/mm/mempolicy/weighted_interleave/node0
$ echo 1 > /sys/kernel/mm/mempolicy/weighted_interleave/node2

# 프로세스에 weighted interleave 정책 적용
# set_mempolicy(MPOL_WEIGHTED_INTERLEAVE, nodemask, maxnode)
$ numactl --weighted-interleave=0,2 ./memory_intensive_app

메모리 티어링 (Memory Tiering)

/*
 * 커널 메모리 티어링 (v5.18+):
 *
 * Tier 0 (Fast): Local DRAM (Node 0, 1)
 * Tier 1 (Slow): CXL Memory (Node 2) / 영구 메모리 (PMEM)
 *
 * 핫 페이지는 Tier 0으로 프로모션
 * 콜드 페이지는 Tier 1로 디모션
 *
 * kswapd가 Tier 0 메모리 부족 시:
 * - 콜드 페이지를 Tier 1으로 디모션 (기존: swap/discard)
 * - Tier 1에서 핫 접근 감지 시 Tier 0으로 프로모션
 */

# 디모션 타겟 설정
$ cat /sys/devices/system/node/node0/memtier
1
$ cat /sys/devices/system/node/node2/memtier
2

# 노드 간 디모션 경로
$ cat /sys/devices/system/node/node0/demotion_targets
2    # Node 0의 콜드 페이지는 Node 2(CXL)로 이동

# 디모션 활성화
$ echo 1 > /sys/kernel/mm/numa/demotion_enabled

# 프로모션 통계
$ cat /proc/vmstat | grep pgpromote
pgpromote_success 12345     # Tier 1 → Tier 0 프로모션 성공
pgdemote_kswapd 67890       # kswapd에 의한 디모션
pgdemote_direct 1234        # direct reclaim에 의한 디모션

11. 가상화와 vNUMA

KVM 가상화 환경에서 게스트 VM에 NUMA 토폴로지를 노출하면 게스트 OS가 NUMA-aware 최적화를 수행할 수 있습니다.

# QEMU/KVM에서 vNUMA 설정
$ qemu-system-x86_64 \
    -smp 16,sockets=2,cores=4,threads=2 \
    -m 16G \
    -object memory-backend-ram,size=8G,id=ram0,host-nodes=0,policy=bind \
    -object memory-backend-ram,size=8G,id=ram1,host-nodes=1,policy=bind \
    -numa node,memdev=ram0,cpus=0-7,nodeid=0 \
    -numa node,memdev=ram1,cpus=8-15,nodeid=1 \
    -numa dist,src=0,dst=1,val=21 \
    ...

# libvirt XML에서 vNUMA 설정
<cpu>
  <numa>
    <cell id='0' cpus='0-7' memory='8388608' unit='KiB'/>
    <cell id='1' cpus='8-15' memory='8388608' unit='KiB'/>
  </numa>
</cpu>
<numatune>
  <memory mode='strict' nodeset='0-1'/>
  <memnode cellid='0' mode='strict' nodeset='0'/>
  <memnode cellid='1' mode='strict' nodeset='1'/>
</numatune>
vNUMA 핀닝: 게스트의 vNUMA 노드를 호스트의 물리 NUMA 노드에 고정(pin)하는 것이 중요합니다. 핀닝 없이 게스트의 vCPU가 호스트의 다른 노드로 마이그레이션되면, 게스트 내부에서 "로컬"이라고 인식한 메모리가 실제로는 호스트의 리모트 노드에 있게 되어 성능이 급격히 저하됩니다.

12. 성능 분석과 최적화

perf로 NUMA 분석

# NUMA 관련 하드웨어 카운터 수집
$ perf stat -e \
    node-loads,node-load-misses,\
    node-stores,node-store-misses \
    -- ./my_application

 Performance counter stats for './my_application':
     142,857,391      node-loads          # 메모리 로드 총 수
       1,234,567      node-load-misses    # 리모트 노드 로드 (0.86%)
      98,234,567      node-stores
         345,678      node-store-misses   # 리모트 노드 스토어

# NUMA 미스 비율이 높은 함수 프로파일링
$ perf record -e node-load-misses -g -- ./my_application
$ perf report --sort=dso,symbol

# perf c2c — NUMA false sharing / remote access 분석
$ perf c2c record -- ./my_application
$ perf c2c report --stdio
=================================================
           Shared Data Cache Line Table
=================================================
       Num     RmtHitm  LclHitm    Stores  ...
  ----- -------- ------- ------- --------
     0      1234     56    7890  ...
     Address: 0x7f8a...
     Source:  my_struct+0x40 (my_module.c:123)

numastat 심화 분석

# 시스템 전체 NUMA 통계
$ numastat
                           node0           node1
numa_hit               142857391       98234567   # 의도한 노드에서 할당 성공
numa_miss                1234567        2345678   # 의도한 노드 실패 → 다른 노드
numa_foreign             2345678        1234567   # 다른 노드의 miss가 여기서 충족
interleave_hit           3456789        3456789   # interleave 정책 적중
local_node             139400824       95888889   # 로컬 CPU에서 로컬 메모리 할당
other_node               4690134        4690356   # 리모트 CPU에서 로컬 메모리 할당

# 높은 numa_miss → NUMA 정책 조정 필요
# 높은 other_node → 태스크가 잘못된 노드에서 실행 중
# numa_miss / numa_hit 비율이 5% 이상이면 최적화 검토

# 프로세스별 NUMA 메모리 분포
$ numastat -p 
$ numastat -c qemu-kvm   # 특정 프로세스 이름으로 조회

최적화 패턴

문제진단해결
높은 리모트 접근 비율numastatnuma_miss 비율 확인numactl --membind 또는 --cpunodebind
NIC IRQ가 리모트 노드 CPU에서 처리cat /proc/irq/*/smp_affinityIRQ를 NIC 로컬 노드 CPU에 바인딩
대형 해시 테이블의 불균일 접근perf c2c 분석numactl --interleave=all
NUMA balancing 오버헤드/proc/vmstatnuma_hint_faultssysctl kernel.numa_balancing=0
한쪽 노드만 OOM/sys/.../node*/meminfo메모리 정책 조정, vm.zone_reclaim_mode
KVM 게스트 성능 저하vCPU가 다른 물리 노드로 이동vNUMA 설정 + CPU/memory 핀닝
CXL 메모리 활용 부족numastat에서 CXL 노드 미사용MPOL_WEIGHTED_INTERLEAVE 정책

13. 커널 설정 종합

# ===== NUMA 관련 커널 설정 종합 =====

# -- 기본 NUMA 지원 --
CONFIG_NUMA=y
CONFIG_AMD_NUMA=y                  # AMD NUMA (K8 이상)
CONFIG_X86_64_ACPI_NUMA=y          # ACPI SRAT 기반 NUMA
CONFIG_ACPI_NUMA=y
CONFIG_NODES_SHIFT=10              # 최대 NUMA 노드 수 (2^10 = 1024)

# -- NUMA Balancing --
CONFIG_NUMA_BALANCING=y             # Automatic NUMA Balancing
CONFIG_NUMA_BALANCING_DEFAULT_ENABLED=y

# -- 메모리 정책 --
CONFIG_MIGRATION=y                  # 페이지 마이그레이션 지원

# -- 메모리 티어링 (CXL 등) --
CONFIG_MEMORY_TIER=y                # 메모리 티어링 프레임워크
CONFIG_DEMOTION=y                   # 콜드 페이지 디모션

# -- CXL 지원 --
CONFIG_CXL_BUS=y
CONFIG_CXL_MEM=y
CONFIG_CXL_ACPI=y
CONFIG_CXL_REGION=y

# -- HMAT (이종 메모리 속성) --
CONFIG_ACPI_HMAT=y                  # HMAT 파싱

# -- 디버깅/통계 --
CONFIG_NUMA_EMU=y                   # NUMA 에뮬레이션 (UMA에서 테스트)
CONFIG_SCHED_DEBUG=y                # 스케줄링 도메인 디버깅
CONFIG_VMSTAT=y                     # /proc/vmstat NUMA 통계
NUMA 에뮬레이션: 단일 소켓(UMA) 시스템에서 NUMA 관련 코드를 테스트하려면 커널 커맨드라인에 numa=fake=4를 추가하여 가상의 4-노드 NUMA 시스템을 에뮬레이션할 수 있습니다. CONFIG_NUMA_EMU=y가 필요합니다.

14. 트러블슈팅

일반적인 문제와 해결

증상원인진단해결
한쪽 노드만 OOM 발생MPOL_BIND로 특정 노드에 바인딩된 프로세스/proc/PID/numa_maps 확인MPOL_PREFERRED로 변경 또는 메모리 증설
성능 저하 (latency 증가)리모트 NUMA 접근 비율 높음perf stat -e node-load-missesnumactl --cpunodebind --membind
NUMA balancing CPU 오버헤드잦은 NUMA hint fault 발생perf top에서 do_numa_page 비율kernel.numa_balancing=0
kswapd 과도한 활동 (한쪽 노드)노드 간 메모리 불균형/proc/zoneinfo 워터마크 확인vm.zone_reclaim_mode 조정
DB 쿼리 성능 불일정NUMA balancing이 페이지 이동/proc/vmstat numa_pages_migratedDB 프로세스에 membind 적용
멀티스레드 앱 확장성 저하스레드 간 false sharing + cross-nodeperf c2c 분석데이터 구조 padding, 노드별 분리

디버깅 명령 모음

# ============ 토폴로지 확인 ============
$ numactl --hardware                  # NUMA 토폴로지 전체
$ lscpu | grep -i numa                # CPU-NUMA 매핑 요약
$ lstopo --of txt                     # hwloc 상세 토폴로지

# ============ 메모리 분포 ============
$ numastat                            # 시스템 전체 NUMA 통계
$ numastat -p                    # 프로세스별 NUMA 메모리
$ cat /proc//numa_maps           # VMA별 노드 분포
$ cat /sys/devices/system/node/node*/meminfo  # 노드별 상세 메모리

# ============ 성능 카운터 ============
$ perf stat -e node-loads,node-load-misses ./app  # NUMA 미스 측정
$ perf c2c record -- ./app            # false sharing 분석

# ============ 밸런싱 통계 ============
$ cat /proc/vmstat | grep numa        # NUMA balancing 통계
$ cat /proc//sched | grep numa   # 프로세스 NUMA 폴트

# ============ 커널 로그 ============
$ dmesg | grep -iE "numa|srat|slit|node"  # NUMA 초기화 로그

# ============ 장치 NUMA 친화성 ============
$ cat /sys/class/net/*/device/numa_node     # NIC NUMA 노드
$ cat /sys/block/*/device/numa_node         # 블록 장치 NUMA 노드
$ lspci -vvv | grep -i "NUMA node"         # PCIe 장치 NUMA 노드