HMM (Heterogeneous Memory Management)
Linux 커널 HMM: CPU와 GPU가 동일한 가상 주소 공간(Address Space)을 공유하는 이기종 메모리 관리(Memory Management) 프레임워크. ZONE_DEVICE, migrate_vma, SVM, DMA-BUF 통합, AI/ML 워크로드 최적화 종합 가이드.
핵심 요약
- 공유 가상 주소(Virtual Address) — CPU와 GPU가 동일한 포인터로 메모리 접근 (제로 카피)
- ZONE_DEVICE — GPU VRAM을 Linux 페이지 프레임(Page Frame) 번호(PFN)로 등록하는 특수 메모리 존
- migrate_vma — CPU 페이지를 GPU 메모리로 투명하게 이동시키는 API
- HMM fault handler — GPU가 없는 페이지에 접근 시 CPU→GPU 마이그레이션 자동 처리
- SVM (Shared Virtual Memory) — ROCm/CUDA Unified Memory의 커널 구현 기반
- DMA-BUF + HMM — NPU/카메라 등 다른 가속기와 제로 카피 버퍼(Buffer) 공유
- NUMA 티어링 — HMM 페이지를 NUMA 계층에 통합하여 자동 promotion/demotion
- hmm_range_fault() — 드라이버가 CPU 페이지 테이블을 읽어 GPU 페이지 테이블 동기화
단계별 이해
- 가상 주소 공간 공유 이해
CPU 프로세스(Process)의 VMA(가상 메모리 영역)를 GPU도 같이 사용하는 개념을 파악합니다. - ZONE_DEVICE 등록 방법 학습
GPU 드라이버가 VRAM을 Linux 메모리 서브시스템에 등록하는 과정을 이해합니다. - migrate_vma API 실습
페이지를 CPU↔GPU 간 이동시키는 API 사용법을 익힙니다. - HMM fault handler 구현
GPU가 없는 페이지에 접근했을 때 자동 마이그레이션이 일어나는 흐름을 추적합니다. - SVM과 ROCm 연동 확인
AMD ROCm의 KFD 드라이버가 HMM을 어떻게 활용하는지 소스에서 확인합니다. - 진단 도구 활용
bpftrace와 /proc/zoneinfo로 페이지 마이그레이션 동작을 관찰합니다.
HMM 개요
HMM(Heterogeneous Memory Management)은 CPU와 GPU가 동일한 가상 주소 공간을 공유하기 위한 Linux 커널 프레임워크입니다. 기존에는 GPU 메모리와 CPU 메모리가 완전히 분리되어 데이터를 사용하기 전에 명시적 복사(cudaMemcpy, clEnqueueCopyBuffer 등)가 필요했지만, HMM을 통해 CPU가 할당한 메모리에 GPU가 직접 접근하거나, GPU VRAM을 CPU가 직접 읽을 수 있습니다.
| 특성 | 전통 GPU 메모리 모델 | HMM 모델 |
|---|---|---|
| 주소 공간 | CPU/GPU 별도 | 공유 가상 주소 |
| 데이터 이동 | 명시적 복사 필요 | 자동 마이그레이션 |
| 포인터 공유 | 불가 (GPU 전용 포인터) | 동일 포인터 사용 |
| 오버헤드(Overhead) | 복사 대역폭(Bandwidth) 비용 | 페이지 폴트(Page Fault) 비용 |
| API | cudaMemcpy, clEnqueue | 일반 malloc/mmap |
| 구현 | 드라이버 독자적 | 커널 mm/hmm.c 공통 |
HMM 발전 역사
HMM은 한 번에 완성된 것이 아니라 여러 커널 버전에 걸쳐 점진적으로 발전했습니다.
초기에는 GPU 드라이버마다 독자적인 메모리 관리 코드를 가졌지만,
mm/hmm.c로 공통 프레임워크가 통합되면서 드라이버 코드가 대폭 간소화되었습니다.
| 커널 버전 | 주요 변경 | 영향 |
|---|---|---|
| v4.14 (2017) | HMM 초기 도입 (mm/hmm.c) | hmm_mirror, hmm_devmem 기본 API 제공 |
| v4.16 (2018) | ZONE_DEVICE MEMORY_DEVICE_PRIVATE 도입 | GPU VRAM을 struct page로 관리 가능 |
| v5.1 (2019) | Nouveau(NVIDIA 오픈소스) HMM 지원 | 첫 업스트림 GPU 드라이버의 HMM 활용 |
| v5.2 (2019) | amdkfd SVM 초기 HMM 통합 | AMD ROCm에서 HMM 기반 공유 가상 메모리 |
| v5.10 (2020) | hmm_range_fault() API 안정화 | hmm_mirror 폐기, mmu_interval_notifier로 전환 |
| v5.14 (2021) | MEMORY_DEVICE_COHERENT 추가 | CPU가 직접 load/store 가능한 장치 메모리 (CXL Type2) |
| v5.16 (2021) | migrate_vma 리팩터링 | migrate_device.c 분리, 3단계 API 확립 |
| v6.1 (2022) | NUMA 메모리 티어링 프레임워크 | HMM 페이지를 NUMA 계층에 통합 |
| v6.3 (2023) | multi-GPU P2P 마이그레이션 | GPU-to-GPU 직접 페이지 이동(Page Migration) 지원 |
| v6.6 (2023) | HMM + Huge Pages 통합 개선 | 2MB/1GB 단위 마이그레이션으로 TLB 효율 향상 |
| v6.8+ (2024) | CXL Type2 + HMM 연동 강화 | CXL 메모리 장치의 coherent HMM 접근 |
hmm_mirror 구조체(Struct)는 완전히 폐기되었고,
현재는 mmu_interval_notifier + hmm_range_fault() 조합이 표준입니다.
드라이버 코드에서 hmm_mirror_register()를 호출하는 코드를 발견하면
v5.10 이전의 구식 코드이므로 참고만 하세요.
HMM 아키텍처
HMM의 핵심은 CPU 페이지 테이블과 GPU 페이지 테이블을 동기화하는 메커니즘입니다.
mmu_notifier를 통해 CPU 페이지 테이블 변경(매핑(Mapping) 해제, 페이지 이동 등)을 GPU 드라이버에 통지합니다.
핵심 구조체
#include <linux/hmm.h>
#include <linux/migrate.h>
#include <linux/mmu_notifier.h>
/* HMM 범위 — 특정 VMA 범위의 페이지 상태 조회 */
struct hmm_range {
struct mmu_notifier_range notifier; /* 시작/끝 주소 */
struct mmu_interval_notifier *notifier_seq; /* 시퀀스 번호 */
unsigned long *hmm_pfns; /* 결과 PFN 배열 */
unsigned long default_flags; /* HMM_PFN_REQ_* */
unsigned long pfn_flags_mask;
unsigned long dev_private_owner; /* 드라이버 식별자 */
};
/* HMM PFN 플래그 */
// HMM_PFN_VALID : 유효한 페이지 (물리 주소 존재)
// HMM_PFN_WRITE : 쓰기 가능 (COW 고려)
// HMM_PFN_ERROR : 접근 오류
// HMM_PFN_NONE : 미매핑 (demand fault 필요)
mmu_notifier — 페이지 테이블 변경 알림
/* GPU 드라이버에서 mmu_notifier 등록 */
static const struct mmu_notifier_ops my_gpu_mmu_ops = {
.invalidate_range_start = my_gpu_invalidate_start,
.invalidate_range_end = my_gpu_invalidate_end,
};
static int my_gpu_invalidate_start(
struct mmu_notifier *mn,
const struct mmu_notifier_range *range)
{
struct my_gpu_ctx *ctx = container_of(mn, struct my_gpu_ctx, notifier);
/* CPU 페이지 테이블 변경 전 GPU MMU를 먼저 무효화 */
my_gpu_unmap_range(ctx, range->start, range->end);
return 0;
}
/* GPU 컨텍스트에 notifier 등록 */
mmu_notifier_register(&ctx->notifier, current->mm);
HMM 페이지 폴트 처리 흐름
HMM의 핵심 동작은 페이지 폴트 기반 마이그레이션입니다. CPU 또는 GPU가 현재 상대방 메모리에 있는 페이지에 접근하면 폴트가 발생하고, HMM 프레임워크가 자동으로 페이지를 적절한 위치로 이동시킵니다.
GPU 페이지 폴트 흐름 (CPU→GPU 마이그레이션)
CPU 페이지 폴트 흐름 (GPU→CPU 마이그레이션)
CPU가 현재 GPU VRAM에 있는 device_private 페이지에 접근하면,
CPU MMU가 폴트를 발생시키고 dev_pagemap_ops.migrate_to_ram 콜백(Callback)이 호출됩니다.
이 콜백은 GPU에서 CPU로 데이터를 DMA 복사한 뒤, 일반 CPU 페이지로 교체합니다.
/* CPU가 device_private 페이지에 접근 → 자동 GPU→CPU 마이그레이션 */
/* mm/memory.c의 handle_pte_fault()에서 호출되는 경로: */
/*
* handle_pte_fault()
* → do_swap_page()
* → is_device_private_entry(entry) → true
* → pgmap->ops->migrate_to_ram(page)
*/
static vm_fault_t my_gpu_migrate_to_ram(struct vm_fault *vmf)
{
struct page *dev_page = vmf->page;
struct my_gpu_dev *gpu = dev_page_to_gpu(dev_page);
struct page *dpage, *spage;
struct migrate_vma args = { 0 };
unsigned long src_pfn, dst_pfn;
/* CPU 측 타겟 페이지 할당 */
dpage = alloc_page_vma(GFP_HIGHUSER, vmf->vma, vmf->address);
if (!dpage)
return VM_FAULT_OOM;
/* migrate_vma 설정: device_private → 일반 페이지 */
args.vma = vmf->vma;
args.src = &src_pfn;
args.dst = &dst_pfn;
args.start = vmf->address;
args.end = vmf->address + PAGE_SIZE;
args.pgmap_owner = gpu;
args.flags = MIGRATE_VMA_SELECT_DEVICE_PRIVATE;
src_pfn = migrate_pfn(page_to_pfn(dev_page)) | MIGRATE_PFN_MIGRATE;
dst_pfn = migrate_pfn(page_to_pfn(dpage));
if (migrate_vma_setup(&args))
goto err;
/* GPU VRAM → CPU DDR: DMA 복사 (동기) */
gpu_dma_copy_to_ram(gpu,
page_to_phys(dpage), /* dst: DDR 물리 주소 */
gpu_vram_addr(dev_page), /* src: VRAM 주소 */
PAGE_SIZE);
migrate_vma_pages(&args);
migrate_vma_finalize(&args);
return 0;
err:
__free_page(dpage);
return VM_FAULT_SIGBUS;
}
preferred_location 속성이나
NVIDIA의 cudaMemAdvise()를 사용해 페이지 고정 위치를 지정하는 것이 권장됩니다.
HMM PFN 플래그 상세
hmm_range_fault()가 반환하는 PFN 배열의 각 항목에는 페이지 상태를 나타내는 플래그가 포함됩니다.
GPU 드라이버는 이 플래그를 해석하여 GPU 페이지 테이블에 적절한 매핑을 생성합니다.
| 플래그 | 비트 | 의미 | 드라이버 동작 |
|---|---|---|---|
HMM_PFN_VALID | bit 0 | 유효한 PFN — 물리 주소(Physical Address) 존재 | GPU PTE에 물리 주소 매핑 |
HMM_PFN_WRITE | bit 1 | 쓰기 가능 — COW 완료 | GPU PTE에 W(쓰기) 비트 설정 |
HMM_PFN_ERROR | bit 2 | 접근 오류 — 읽기 불가 | 해당 VA 접근 금지 설정 |
HMM_PFN_ORDER_SHIFT | bit 56-63 | 복합 페이지 크기 (huge page) | GPU 대형 페이지 매핑 가능 |
HMM_PFN_REQ_FAULT | 입력 플래그 | 페이지 폴트 강제 (demand paging) | default_flags에 설정 |
HMM_PFN_REQ_WRITE | 입력 플래그 | 쓰기 접근 요청 (COW 트리거) | default_flags에 설정 |
/* PFN 플래그 해석 예시 — GPU 페이지 테이블 빌더 */
static void gpu_build_ptes(struct my_gpu_ctx *ctx,
unsigned long *pfns,
unsigned long npages,
unsigned long va_start)
{
unsigned long i;
for (i = 0; i < npages; i++) {
unsigned long pfn = pfns[i];
unsigned long va = va_start + (i << PAGE_SHIFT);
if (!(pfn & HMM_PFN_VALID)) {
/* 미매핑 — GPU PTE도 무효화 */
gpu_pte_clear(ctx, va);
continue;
}
if (pfn & HMM_PFN_ERROR) {
/* 오류 페이지 — 접근 시 GPU 예외 발생하도록 */
gpu_pte_set_poison(ctx, va);
continue;
}
uint64_t phys = hmm_pfn_to_phys(pfn);
uint64_t flags = GPU_PTE_READABLE;
if (pfn & HMM_PFN_WRITE)
flags |= GPU_PTE_WRITABLE;
/* huge page 감지 (order > 0이면 대형 페이지 매핑) */
unsigned int order = hmm_pfn_to_map_order(pfn);
if (order >= 9) /* 2MB 이상 */
flags |= GPU_PTE_HUGE_2M;
gpu_pte_set(ctx, va, phys, flags);
}
}
ZONE_DEVICE와 device_private 페이지
ZONE_DEVICE는 GPU VRAM, PMEM(영구 메모리) 등 CPU 직접 접근이 제한된 장치 메모리를
Linux 페이지 프레임 번호(PFN) 시스템에 통합하는 특수 메모리 존입니다.
이를 통해 커널 메모리 관리 코드가 GPU 메모리 페이지를 일반 페이지처럼 다룰 수 있습니다.
ZONE_DEVICE 등록 절차
#include <linux/memremap.h>
/* GPU 드라이버 초기화 시 VRAM을 ZONE_DEVICE로 등록 */
static int my_gpu_register_vram(struct my_gpu_dev *gpu)
{
struct dev_pagemap *pgmap = &gpu->pgmap;
pgmap->type = MEMORY_DEVICE_PRIVATE; /* GPU 전용 메모리 */
pgmap->range.start = gpu->vram_phys_base;
pgmap->range.end = gpu->vram_phys_base + gpu->vram_size - 1;
pgmap->nr_range = 1;
pgmap->ops = &my_gpu_pgmap_ops; /* migrate_to_ram 등 */
/* VRAM 물리 주소를 struct page 배열에 연결 */
gpu->vram_pages = memremap_pages(pgmap, dev_to_node(gpu->dev));
if (IS_ERR(gpu->vram_pages))
return PTR_ERR(gpu->vram_pages);
return 0;
}
/* dev_pagemap 오퍼레이션 — 페이지를 CPU로 복구 */
static const struct dev_pagemap_ops my_gpu_pgmap_ops = {
/* GPU → CPU 마이그레이션 (swapout 등에서 호출) */
.migrate_to_ram = my_gpu_migrate_to_ram,
/* device_private 페이지 reference count 0 시 호출 */
.page_free = my_gpu_page_free,
};
| ZONE_DEVICE 유형 | 용도 | 접근 방식 |
|---|---|---|
MEMORY_DEVICE_PRIVATE | GPU VRAM (CPU 직접 접근 불가) | migrate_vma로만 접근 |
MEMORY_DEVICE_COHERENT | CXL Type2, GPU UMAP 영역 | CPU load/store 가능 |
MEMORY_DEVICE_FS_DAX | DAX 파일시스템 (PMEM) | DAX mmap |
MEMORY_DEVICE_GENERIC | 일반 장치 메모리 | DMA 전용 |
Coherent Device Memory (MEMORY_DEVICE_COHERENT)
MEMORY_DEVICE_COHERENT는 v5.14에서 도입된 ZONE_DEVICE 유형으로,
CPU가 직접 load/store로 접근 가능한 장치 메모리를 지원합니다.
MEMORY_DEVICE_PRIVATE과 달리 CPU 접근 시 자동 마이그레이션이 필요 없으며,
CXL Type2 장치나 GPU의 CPU-접근 가능 VRAM 영역(일명 "doorbell" 또는 "system" 영역)에 사용됩니다.
| 특성 | MEMORY_DEVICE_PRIVATE | MEMORY_DEVICE_COHERENT |
|---|---|---|
| CPU 직접 접근 | 불가 (fault → migrate_to_ram) | 가능 (load/store) |
| 대표 장치 | GPU VRAM (discrete) | CXL Type2, APU 통합 메모리 |
| 주소 공간 | CPU에서 비가시 | CPU 물리 주소 공간에 매핑 |
| 마이그레이션 | migrate_vma 필수 | 선택적 (성능 최적화용) |
| 캐시 일관성(Cache Coherency) | 없음 (DMA만) | HW 코히런시 (CXL.cache) |
| NUMA 노드 | 별도 노드 (접근 불가) | 별도 노드 (접근 가능, 높은 지연) |
/* MEMORY_DEVICE_COHERENT 등록 (CXL Type2 가속기) */
static int cxl_accel_register_coherent(struct cxl_accel *accel)
{
struct dev_pagemap *pgmap = &accel->pgmap;
pgmap->type = MEMORY_DEVICE_COHERENT;
pgmap->range.start = accel->mem_phys_base;
pgmap->range.end = accel->mem_phys_base + accel->mem_size - 1;
pgmap->nr_range = 1;
pgmap->ops = &cxl_coherent_pgmap_ops;
/* CPU가 직접 접근 가능 — NUMA 노드로 등록 */
accel->mem_pages = memremap_pages(pgmap,
dev_to_node(accel->dev));
if (IS_ERR(accel->mem_pages))
return PTR_ERR(accel->mem_pages);
/* memory_tier에 등록 (demotion 대상) */
set_node_memory_tier(dev_to_node(accel->dev), 1); /* tier 1 */
pr_info("CXL accel: %zu MB coherent memory on NUMA node %d\n",
accel->mem_size >> 20, dev_to_node(accel->dev));
return 0;
}
/* Coherent 페이지는 migrate_to_ram 대신 직접 접근 */
static const struct dev_pagemap_ops cxl_coherent_pgmap_ops = {
/* migrate_to_ram은 NULL — CPU가 직접 접근 가능 */
.page_free = cxl_coherent_page_free,
};
CXL.cache 프로토콜로
CPU 캐시(Cache)와 일관성을 유지하면서 자체 메모리를 노출합니다.
이 메모리를 MEMORY_DEVICE_COHERENT로 등록하면 CPU가 직접 접근하되,
자주 접근하는 페이지만 migrate_vma로 DDR로 승격(promotion)하여 지연 시간을 줄일 수 있습니다.
자세한 내용은 CXL 메모리 문서를 참고하세요.
SVM과 AMD HSA/ROCm 통합
SVM(Shared Virtual Memory)은 CPU와 GPU가 동일한 가상 주소를 사용하는 프로그래밍 모델입니다. AMD의 HSA(Heterogeneous System Architecture)와 ROCm의 KFD(Kernel Fusion Driver)가 Linux HMM을 기반으로 SVM을 구현합니다.
ROCm KFD SVM 구현
/* drivers/gpu/drm/amd/amdkfd/kfd_svm.c 핵심 흐름 */
/* SVM 영역 등록 — ROCm runtime이 호출 */
int svm_range_add(struct kfd_process *p,
uint64_t start, uint64_t size,
uint32_t nattr,
struct kfd_ioctl_svm_attribute *attrs)
{
struct svm_range *prange;
/* HMM mmu_interval_notifier 등록 */
mmu_interval_notifier_insert(&prange->notifier,
mm, start, size,
&svm_range_mn_ops);
/* NUMA 선호도, 마이그레이션 정책 설정 */
svm_range_set_attr(p, mm, start, size, nattr, attrs);
return 0;
}
/* GPU 페이지 폴트 핸들러 */
static int svm_range_restore_pages(struct amdgpu_device *adev,
unsigned int pasid,
uint64_t addr)
{
/* 1. CPU 페이지 테이블 조회 (hmm_range_fault) */
hmm_range_fault(&range);
/* 2. CPU 메모리 → GPU VRAM 마이그레이션 */
svm_migrate_to_vram(prange, addr, adev, mm);
/* 3. GPU 페이지 테이블 업데이트 */
svm_range_map_to_gpu(adev, mm, prange, addr, 1, NULL);
return 0;
}
cudaMallocManaged()로 할당하고, 페이지 폴트 시 자동 마이그레이션이 이루어집니다.
AMD ROCm은 동일한 패턴을 Linux HMM 표준 API 위에 구현하여 커널 업스트림과 더 긴밀하게 통합됩니다.
DMA-BUF와 HMM 결합
DMA-BUF는 여러 디바이스 드라이버가 동일한 메모리 버퍼를 공유하는 메커니즘입니다. HMM과 결합하면 GPU 메모리, NPU 메모리, 카메라 버퍼가 모두 같은 물리 메모리(Physical Memory)를 참조할 수 있어 제로 카피 AI 파이프라인(Pipeline)을 구현할 수 있습니다.
HMM + DMA-BUF 통합 패턴
/* GPU 메모리를 DMA-BUF로 export (NPU/카메라와 공유) */
static struct dma_buf *my_gpu_gem_export(
struct drm_gem_object *obj,
int flags)
{
DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
exp_info.ops = &my_gpu_dma_buf_ops;
exp_info.size = obj->size;
exp_info.flags = flags;
exp_info.priv = obj;
return dma_buf_export(&exp_info);
}
/* NPU 드라이버에서 GPU 버퍼를 import하여 HMM 범위 조회 */
int npu_import_gpu_buffer(struct dma_buf *dmabuf)
{
struct dma_buf_attachment *attach;
struct sg_table *sgt;
attach = dma_buf_attach(dmabuf, npu->dev);
sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
/* sg_table의 물리 주소를 NPU IOMMU에 매핑 */
npu_iommu_map_sg(npu, sgt);
return 0;
}
| 시나리오 | 관련 API | 메모리 이동 |
|---|---|---|
| GPU → NPU 공유 | DMA-BUF export/import | 없음 (동일 VRAM) |
| GPU VRAM → CPU 접근 | migrate_vma (VRAM→DDR) | PCIe 복사 |
| 카메라 → GPU 직접 | DMA-BUF + IOMMU | 없음 (DMA) |
| CPU malloc → GPU 연산 | HMM fault + migrate | PCIe 마이그레이션 |
migrate_vma 마이그레이션 흐름
페이지를 CPU 메모리에서 GPU VRAM으로 이동시키는 전체 흐름을 설명합니다.
migrate_vma_setup() → migrate_vma_pages() → migrate_vma_finalize() 3단계로 구성됩니다.
/* GPU 드라이버에서 migrate_vma 사용 예시 */
static int gpu_migrate_to_vram(struct my_gpu *gpu,
struct vm_area_struct *vma,
unsigned long start,
unsigned long end)
{
struct migrate_vma args = {
.vma = vma,
.dst = dst_pages, /* GPU VRAM 목표 PFN 배열 */
.src = src_pages, /* CPU 소스 PFN 배열 */
.start = start,
.end = end,
.pgmap_owner = gpu, /* device_private 소유자 */
.flags = MIGRATE_VMA_SELECT_SYSTEM, /* CPU 페이지만 선택 */
};
/* 단계 1: 소스 페이지 조회 및 락 */
int ret = migrate_vma_setup(&args);
if (ret) return ret;
/* 단계 2: GPU VRAM 페이지 할당 및 DMA 복사 */
gpu_alloc_and_copy(gpu, &args);
/* 단계 3: CPU 페이지 테이블 업데이트 완료 */
migrate_vma_pages(&args);
migrate_vma_finalize(&args);
return 0;
}
NUMA 티어링과 HMM
Linux 6.1+의 NUMA 메모리 티어링(Memory Tiering) 프레임워크는 HMM과 협력하여 빠른 메모리(DDR5)에서 느린 메모리(CXL, GPU VRAM)로 덜 쓰이는 페이지를 자동 demotion하고, 자주 접근하는 페이지는 faster tier로 promotion합니다.
# NUMA 메모리 티어링 설정
# 자동 NUMA 밸런싱 활성화 (HMM과 함께 동작)
echo 1 > /proc/sys/kernel/numa_balancing
# 메모리 티어 구조 확인
cat /sys/devices/virtual/memory_tiering/memory_tier0/nodelist
cat /sys/devices/virtual/memory_tiering/memory_tier1/nodelist
# 프로세스를 Tier1(CXL)에서 실행
numactl --membind=2 ./ai_inference_job
# 특정 프로세스의 메모리를 GPU 노드로 이동
# (migrate_pages 시스콜 사용)
move_pages $(pidof my_app) 1 NULL 3 NULL MPOL_MF_MOVE_ALL
Multi-GPU P2P 마이그레이션
AI/ML 워크로드에서는 여러 GPU가 동시에 같은 데이터에 접근하거나, 모델 파라미터를 GPU 간에 분산해야 하는 경우가 흔합니다. HMM은 GPU-to-GPU 직접 페이지 마이그레이션(P2P migration)을 지원하여 CPU 메모리를 경유하지 않고 GPU 간 직접 데이터를 이동시킬 수 있습니다.
/* GPU-to-GPU P2P 마이그레이션 (GPU0 → GPU1) */
static int gpu_p2p_migrate(struct my_gpu *src_gpu,
struct my_gpu *dst_gpu,
struct vm_area_struct *vma,
unsigned long start,
unsigned long end)
{
struct migrate_vma args = {
.vma = vma,
.src = src_pfns,
.dst = dst_pfns,
.start = start,
.end = end,
.pgmap_owner = src_gpu, /* 소스 GPU의 device_private만 선택 */
.flags = MIGRATE_VMA_SELECT_DEVICE_PRIVATE,
};
int ret = migrate_vma_setup(&args);
if (ret) return ret;
/* 목적지 GPU에서 페이지 할당 */
gpu_alloc_device_pages(dst_gpu, dst_pfns, npages);
/* P2P DMA 복사 (PCIe BAR-to-BAR 또는 NVLink) */
gpu_p2p_dma_copy(src_gpu, dst_gpu,
src_pfns, dst_pfns, npages);
migrate_vma_pages(&args);
migrate_vma_finalize(&args);
/* CPU PTE: src_gpu device_private → dst_gpu device_private */
/* GPU1 PTE: VA → VRAM1 물리 주소 */
return 0;
}
CONFIG_PCI_P2PDMA=y 커널 옵션이 필요하며,
pci_p2pdma_distance()로 P2P 가능 여부와 예상 대역폭을 확인할 수 있습니다.
NVIDIA NVLink나 AMD xGMI 같은 전용 인터커넥트는 PCIe보다 훨씬 높은 대역폭을 제공합니다.
성능 최적화와 Thrashing 방지
HMM 기반 공유 가상 메모리의 최대 장점은 프로그래밍 편의성이지만, 페이지 마이그레이션 비용을 고려하지 않으면 오히려 명시적 복사보다 성능이 나빠질 수 있습니다. 이 섹션에서는 HMM 성능을 최적화하는 실전 기법을 다룹니다.
Thrashing 문제와 해결
CPU와 GPU가 교대로 같은 페이지에 접근하면, 페이지가 CPU↔GPU 사이를 반복적으로 이동합니다. 이를 thrashing이라 하며, 매 이동마다 PCIe DMA 복사 비용(수~수십 마이크로초)이 발생합니다.
| 해결 기법 | 설명 | API / 설정 |
|---|---|---|
| 선호 위치 지정 | 페이지의 기본 위치를 CPU 또는 특정 GPU로 고정 | ROCm: KFD_IOCTL_SVM_ATTR_PREFERRED_LOC |
| 접근 카운터 | GPU HW 접근 카운터로 실제 핫 페이지만 마이그레이션 | NVIDIA: access counter, AMD: XNACK retry |
| 프리페치 | 실행 전에 필요한 페이지를 미리 마이그레이션 | ROCm: svm_migrate_to_vram() 명시적 호출 |
| 읽기 전용(Read-Only) 복제 | 읽기만 하는 페이지는 CPU/GPU 양쪽에 복사본 유지 | HMM_PFN_VALID (쓰기 플래그 없음) |
| 대형 페이지 | 2MB 단위 마이그레이션으로 TLB 미스/폴트 수 감소 | CONFIG_TRANSPARENT_HUGEPAGE + HMM |
| 마이그레이션 임계값 | 일정 폴트 횟수 이상일 때만 마이그레이션 수행 | 드라이버 정책 (fault counter) |
/* ROCm SVM: 선호 위치 및 접근 정책 설정 (유저스페이스) */
#include <hsakmt.h>
/* 특정 메모리 범위를 GPU 0에 고정 */
HSAuint32 gpu_id = 0;
HsaMemFlags flags;
struct kfd_ioctl_svm_attribute attrs[] = {
{ .type = KFD_IOCTL_SVM_ATTR_PREFERRED_LOC,
.value = gpu_id },
{ .type = KFD_IOCTL_SVM_ATTR_SET_FLAGS,
.value = KFD_IOCTL_SVM_FLAG_GPU_RO }, /* GPU 읽기 전용 */
};
/* 커널 SVM에 속성 전달 */
hsaKmtSVMSetAttr(ptr, size,
2, /* nattr */
attrs);
/* 명시적 프리페치 (AI 배치 시작 전) */
hsaKmtSVMSetAttr(model_weights, weight_size,
1,
&(struct kfd_ioctl_svm_attribute){
.type = KFD_IOCTL_SVM_ATTR_PREFETCH_LOC,
.value = gpu_id
});
성능 측정 메트릭
# HMM 마이그레이션 횟수 및 바이트 추적
bpftrace -e '
kretprobe:migrate_vma_finalize {
@migrations = count();
}
kprobe:migrate_vma_setup {
/* start와 end의 차이로 마이그레이션 크기 추적 */
@bytes = hist(((struct migrate_vma *)arg0)->end -
((struct migrate_vma *)arg0)->start);
}'
# PCIe DMA 대역폭 사용량 확인 (perf)
perf stat -e 'uncore_iio/event=0x83,umask=0x04/' \
-e 'uncore_iio/event=0xc0,umask=0x04/' \
-p $(pidof my_app) -- sleep 10
# AMD GPU의 XNACK(재시도) 폴트 통계
cat /sys/class/drm/card0/device/ras/gpu_fault_count
cat /sys/kernel/debug/dri/0/amdgpu_vm_info
드라이버 API — hmm_range_fault()
hmm_range_fault()는 GPU 드라이버가 CPU 페이지 테이블의 현재 상태를 조회하여
GPU 페이지 테이블을 동기화할 때 사용하는 핵심 API입니다.
CPU 페이지가 스왑(Swap) 아웃되어 있으면 자동으로 스왑 인도 수행합니다.
/* hmm_range_fault() 사용 예시 — CPU → GPU PTE 동기화 */
static int gpu_sync_page_table(struct my_gpu_ctx *ctx,
unsigned long start,
unsigned long end)
{
unsigned long npages = (end - start) >> PAGE_SHIFT;
unsigned long *pfns;
struct hmm_range range;
int ret;
pfns = kvmalloc_array(npages, sizeof(*pfns), GFP_KERNEL);
range.notifier = &ctx->interval_notifier;
range.start = start;
range.end = end;
range.hmm_pfns = pfns;
range.default_flags = HMM_PFN_REQ_FAULT | HMM_PFN_REQ_WRITE;
retry:
range.notifier_seq = mmu_interval_read_begin(range.notifier);
mmap_read_lock(ctx->mm);
ret = hmm_range_fault(&range); /* CPU 페이지 테이블 조회 + fault */
mmap_read_unlock(ctx->mm);
if (ret == -EBUSY) goto retry; /* 동시 수정 발생 시 재시도 */
if (ret) goto err;
/* 시퀀스 번호로 동시성 확인 */
if (mmu_interval_read_retry(range.notifier, range.notifier_seq))
goto retry;
/* pfns 배열로 GPU 페이지 테이블 업데이트 */
gpu_update_page_table(ctx, pfns, npages, start);
err:
kvfree(pfns);
return ret;
}
mmu_interval_notifier
/* mmu_interval_notifier: 특정 VMA 구간 변경 감지 */
static const struct mmu_interval_notifier_ops gpu_mn_ops = {
.invalidate = gpu_mn_invalidate,
};
static bool gpu_mn_invalidate(
struct mmu_interval_notifier *mni,
const struct mmu_notifier_range *range,
unsigned long cur_seq)
{
/* 시퀀스 번호 기록 (hmm_range_fault retry 감지용) */
mmu_interval_set_seq(mni, cur_seq);
/* GPU 해당 VA 범위 flush (비동기 가능) */
gpu_tlb_flush_range(mni->start, mni->end);
return true;
}
/* GPU 컨텍스트 생성 시 등록 */
mmu_interval_notifier_insert(&ctx->interval_notifier, mm,
vma_start, vma_size, &gpu_mn_ops);
진단 및 디버깅(Debugging)
procfs / sysfs 진단
# ZONE_DEVICE 통계 확인
cat /proc/zoneinfo | grep -A20 "zone Device"
# 특정 프로세스의 HMM 관련 페이지 맵 확인
cat /proc/$(pidof my_app)/smaps | grep -A15 "Huge"
# NUMA 페이지 마이그레이션 통계
cat /proc/vmstat | grep numa_
# numa_page_migrated : 마이그레이션된 페이지 수
# numa_hint_faults : NUMA 힌트 폴트 수
# GPU 드라이버 HMM 통계 (amdgpu 예시)
cat /sys/kernel/debug/dri/0/amdgpu_vm_info
bpftrace로 마이그레이션 추적
# migrate_vma_setup 호출 추적
bpftrace -e '
kprobe:migrate_vma_setup {
printf("[HMM] migrate_vma_setup: start=%lx end=%lx pid=%d\n",
((struct migrate_vma *)arg0)->start,
((struct migrate_vma *)arg0)->end,
pid);
}'
# hmm_range_fault 지연 시간 측정
bpftrace -e '
kprobe:hmm_range_fault { @start[tid] = nsecs; }
kretprobe:hmm_range_fault /@start[tid]/ {
@latency = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
# ZONE_DEVICE 페이지 폴트 이벤트
bpftrace -e '
tracepoint:migrate:mm_migrate_pages {
printf("migrated: mode=%d pages=%lu\n", args->mode, args->nr_succeeded);
}'
ftrace HMM 이벤트
# ftrace로 HMM 관련 이벤트 활성화
cd /sys/kernel/debug/tracing
echo 1 > events/migrate/enable
echo 1 > events/mmu_notifier/enable
echo 1 > tracing_on
cat trace_pipe | grep hmm
HMM 테스트 모듈 (lib/test_hmm.c)
Linux 커널은 HMM API를 검증하기 위한 자체 테스트 모듈 lib/test_hmm.c를 포함합니다.
이 모듈은 가상의 디바이스를 생성하여 ZONE_DEVICE, migrate_vma,
hmm_range_fault() 등 모든 HMM API를 실제 GPU 없이 테스트할 수 있게 합니다.
HMM 드라이버를 개발할 때 참고 코드로 매우 유용합니다.
/* lib/test_hmm.c — HMM 테스트 모듈의 핵심 구조 */
/* 가상 디바이스 구조체 */
struct dmirror_device {
struct cdev cdevice; /* 문자 디바이스 */
struct dev_pagemap pagemap; /* ZONE_DEVICE 관리 */
unsigned long *pfns; /* HMM PFN 배열 */
void *mdevice_pages; /* 가상 VRAM */
struct mutex devmem_lock;
};
/* 테스트용 ioctl 명령 */
#define HMM_DMIRROR_READ _IOWR('H', 0x00, struct hmm_dmirror_cmd)
#define HMM_DMIRROR_WRITE _IOWR('H', 0x01, struct hmm_dmirror_cmd)
#define HMM_DMIRROR_MIGRATE_TO_DEV _IOWR('H', 0x02, struct hmm_dmirror_cmd)
#define HMM_DMIRROR_MIGRATE_TO_SYS _IOWR('H', 0x03, struct hmm_dmirror_cmd)
#define HMM_DMIRROR_SNAPSHOT _IOWR('H', 0x04, struct hmm_dmirror_cmd)
/* migrate_to_ram 콜백 — 테스트 디바이스에서 CPU로 복귀 */
static vm_fault_t dmirror_devmem_fault(struct vm_fault *vmf)
{
struct dmirror_device *mdevice;
unsigned long src, dst;
mdevice = dmirror_page_to_device(vmf->page);
/* 가상 VRAM → CPU 메모리 복사 */
memcpy(page_address(dpage),
dmirror_devmem_addr(vmf->page),
PAGE_SIZE);
/* ... migrate_vma 호출 ... */
return 0;
}
# HMM 테스트 모듈 빌드 및 실행
# 커널 빌드 시 테스트 모듈 활성화
make menuconfig
# Kernel hacking → Memory Debugging → HMM test module
# CONFIG_TEST_HMM=m
# 모듈 로드
modprobe test_hmm
# 유저스페이스 테스트 실행
cd tools/testing/selftests/vm
make hmm-tests
./hmm-tests
# 개별 테스트 실행
./hmm-tests -t migrate_to_device
./hmm-tests -t snapshot
./hmm-tests -t fault
# 테스트 결과 확인
dmesg | tail -50
lib/test_hmm.c를 읽고 HMM API 사용 패턴 파악tools/testing/selftests/vm/hmm-tests.c로 유저스페이스 테스트 코드 참고drivers/gpu/drm/amd/amdkfd/kfd_svm.c에서 실전 드라이버 구현 확인- 자체 드라이버에 HMM 통합 시
test_hmm의 테스트를 기반으로 검증
보안 고려사항
HMM은 GPU와 CPU 간 메모리를 공유하므로, 보안에 미치는 영향이 큽니다.
특히 device_private 페이지는 CPU에서 직접 접근할 수 없어
전통적인 메모리 보호 메커니즘이 적용되지 않는 영역이 생깁니다.
| 보안 위협 | 설명 | 커널 대응 |
|---|---|---|
| GPU 메모리 유출 | 이전 프로세스의 GPU 페이지가 새 프로세스에 노출 | page_free 콜백에서 VRAM 제로화 필수 |
| DMA 버퍼 권한 위회 | DMA-BUF를 통해 다른 프로세스의 GPU 메모리 접근 | DMA-BUF fd 전달은 SCM_RIGHTS로만 제한 |
| mmu_notifier 경쟁 | invalidate와 GPU 접근 사이의 시간 창 | invalidate_range_start에서 GPU 중지 필수 |
| ptrace 제한 | device_private 페이지는 ptrace로 읽기 불가 | access_device_entry()에서 migrate_to_ram 강제 |
| SELinux/AppArmor | GPU 메모리 접근에 대한 MAC 정책 부재 | DRM render 노드 접근 제어(Access Control)로 간접 제한 |
| 사이드 채널 | GPU 페이지 폴트 타이밍으로 접근 패턴 유추 | GPU 드라이버의 폴트 통계 접근 제한 |
/* GPU 페이지 해제 시 보안 제로화 */
static void my_gpu_page_free(struct page *page)
{
struct my_gpu_dev *gpu = page_to_gpu(page);
/* VRAM 내용을 0으로 초기화 (정보 유출 방지) */
gpu_memset_vram(gpu, page_to_vram_offset(page),
0, PAGE_SIZE);
/* GPU 내부 TLB에서도 해당 매핑 제거 */
gpu_tlb_invalidate(gpu, page);
/* VRAM 페이지 풀에 반환 */
gpu_free_vram_page(gpu, page);
}
/* mmu_notifier invalidate에서 GPU 접근 차단 */
static int my_gpu_invalidate_start(
struct mmu_notifier *mn,
const struct mmu_notifier_range *range)
{
struct my_gpu_ctx *ctx = container_of(mn, struct my_gpu_ctx, mn);
/* GPU 엔진 일시 정지 (invalidate와 접근 사이의 경쟁 방지) */
gpu_engine_pause(ctx);
/* 해당 범위의 GPU PTE 무효화 */
gpu_pte_invalidate_range(ctx, range->start, range->end);
/* GPU TLB 플러시 (shoot-down) */
gpu_tlb_flush(ctx);
return 0;
}
device_private 페이지는 자동으로 CPU로 마이그레이션되어
코어 파일에 포함됩니다. 이는 GPU 메모리에 민감한 데이터(AI 모델 가중치, 암호화(Encryption) 키 등)가 있을 경우
의도치 않은 정보 유출 가능성이 있습니다. MADV_DONTDUMP으로 해당 VMA를 코어 덤프에서 제외하세요.
커널 소스 가이드
| 파일 / 디렉토리 | 설명 |
|---|---|
mm/hmm.c | HMM 핵심 구현 — hmm_range_fault(), mmu_notifier 통합 |
mm/migrate_device.c | migrate_vma_setup/pages/finalize() 구현 |
mm/memremap.c | memremap_pages(), ZONE_DEVICE 등록 |
include/linux/hmm.h | HMM 공개 API — hmm_range, HMM_PFN_* 플래그 |
include/linux/memremap.h | dev_pagemap, dev_pagemap_ops 구조체 |
include/linux/mmu_notifier.h | mmu_notifier_ops, mmu_interval_notifier |
drivers/gpu/drm/amd/amdkfd/kfd_svm.c | AMD ROCm SVM 구현 (HMM 활용 참고) |
drivers/gpu/drm/amd/amdkfd/kfd_migrate.c | KFD GPU↔CPU 마이그레이션 로직 |
drivers/gpu/drm/nouveau/ | Nouveau HMM 구현 (NVIDIA 오픈소스) |
lib/test_hmm.c | HMM 테스트 모듈 |
커널 설정
# HMM 활성화 (GPU 드라이버 선택 시 자동으로 선택됨)
CONFIG_HMM_MIRROR=y # HMM 미러 (mmu_notifier 기반)
CONFIG_DEVICE_PRIVATE=y # ZONE_DEVICE device_private 지원
CONFIG_DEVICE_PUBLIC=y # ZONE_DEVICE coherent 지원
CONFIG_MIGRATE=y # 페이지 마이그레이션 지원
CONFIG_NUMA=y # NUMA 지원 (티어링에 필요)
CONFIG_NUMA_BALANCING=y # 자동 NUMA 밸런싱
# AMD ROCm (HMM 주 사용자)
CONFIG_HSA_AMD=y
CONFIG_DRM_AMDGPU=y
# PMEM HMM
CONFIG_ZONE_DEVICE=y
CONFIG_FS_DAX=y
실전 사용 사례
대규모 언어 모델 (LLM) 추론
GPT-4급 LLM 모델은 파라미터가 수백 GB에 달하여 단일 GPU VRAM에 담을 수 없습니다. HMM을 활용하면 모델 파라미터를 CPU DDR + GPU VRAM에 분산 배치하고, 현재 추론 레이어에 필요한 파라미터만 GPU로 자동 마이그레이션할 수 있습니다.
/* LLM 추론에서 HMM 활용 패턴 (유저스페이스 관점) */
#include <hip/hip_runtime.h>
/* Unified Memory로 모델 파라미터 할당 (HMM 기반) */
float *model_weights;
hipMallocManaged(&model_weights, 200ULL * 1024 * 1024 * 1024); /* 200 GB */
/* 모델 가중치를 CPU 메모리에 로드 (mmap + read) */
load_model_from_disk(model_weights, "model.safetensors");
/* GPU에서 추론 실행 — 필요한 레이어가 자동 마이그레이션 */
for (int layer = 0; layer < num_layers; layer++) {
float *layer_weights = model_weights + layer_offsets[layer];
size_t layer_size = layer_sizes[layer];
/* 선호 위치 힌트: 이 레이어를 GPU로 프리페치 */
hipMemPrefetchAsync(layer_weights, layer_size, gpu_device);
/* GPU 커널 실행 (HMM이 나머지 페이지 폴트 처리) */
transformer_layer_kernel<<<grid, block>>>(
layer_weights, activations, layer);
}
/* 사용 완료 후 해제 — GPU와 CPU 페이지 모두 자동 정리 */
hipFree(model_weights);
과학 계산 (HPC)
기상 시뮬레이션, 분자 동역학 등 HPC 워크로드에서는 거대한 3D 격자(grid) 데이터를 CPU와 GPU가 교대로 처리합니다. HMM은 경계 교환(halo exchange) 패턴에서 명시적 복사 코드를 제거하고 코드 복잡도를 크게 줄여줍니다.
| 사용 사례 | 데이터 규모 | HMM 이점 | 주의사항 |
|---|---|---|---|
| LLM 추론 | 100~1000 GB | VRAM 초과 모델 실행 가능 | 레이어별 프리페치 필수 |
| LLM 학습 | 수십~수백 GB | gradient 동기화 간소화 | thrashing 주의 (optimizer state) |
| 이미지 처리 | 수 GB | 제로 카피 파이프라인 | DMA-BUF 결합 시 최적 |
| 과학 계산 | 수십~수백 GB | halo exchange 단순화 | 경계 영역 thrashing 주의 |
| 데이터베이스 | 수~수십 GB | GPU 가속 쿼리 | 인덱스 스캔 시 폴트 폭주 주의 |
| 비디오 인코딩 | 수 GB | 카메라→GPU 제로 카피 | 실시간(Real-time) 제약 (폴트 지연 예산) |
카메라 → GPU → NPU 파이프라인
엣지 AI 디바이스에서는 카메라 센서 데이터가 GPU에서 전처리된 후 NPU에서 추론됩니다. HMM과 DMA-BUF를 결합하면 이 전체 파이프라인을 제로 카피로 구현할 수 있습니다.
HMM 통합 체크리스트
HMM 도입의 핵심 리스크는 CPU/GPU 페이지 테이블 불일치와 마이그레이션 경쟁 조건(Race Condition)입니다. 드라이버 통합 시 mmu_notifier 경로와 fault 경로를 함께 검증해야 합니다.
| 검사 항목 | 질문 | 점검 포인트 |
|---|---|---|
| 페이지 동기화 | CPU 매핑 변경이 GPU에 반영되는가? | mmu_notifier invalidate 경로 |
| 마이그레이션 | CPU↔디바이스 이동 후 접근 권한이 맞는가? | migrate_vma finalize 경로 |
| fault 처리 | 디바이스 fault에서 복구 가능한가? | hmm_range_fault 결과 처리 |
| 회수/해제 | 프로세스 종료 시 누수 없는가? | dev_pagemap 참조 해제 검증 |
# HMM 관련 로그/코드 경로 점검
dmesg | grep -Ei "hmm|migrate_vma|device_private"
git grep -n "hmm_range_fault\\|migrate_vma" -- mm drivers/gpu
MEMORY_DEVICE_PRIVATE vs COHERENT
ZONE_DEVICE는 장치 메모리를 struct page로 관리하기 위한 특수 존이지만,
장치 메모리의 코히어런시 모델에 따라 세 가지 memory_type이 구분됩니다.
드라이버가 잘못된 타입을 선택하면 커널 패닉(Kernel Panic)이나 데이터 손상이 발생할 수 있으므로 정확한 이해가 필수입니다.
| 속성 | MEMORY_DEVICE_PRIVATE | MEMORY_DEVICE_COHERENT | MEMORY_DEVICE_GENERIC |
|---|---|---|---|
| 대표 디바이스 | GPU VRAM (NVIDIA, AMD) | CXL Type2 가속기 | DAX (persistent memory) |
| CPU 직접 접근 | 불가 — fault시 마이그레이션 필수 | 가능 — load/store 직접 수행 | 가능 — mmap으로 매핑 |
| 코히어런시 | 없음 (GPU 전용) | 하드웨어 코히어런트 | N/A (DAX) |
| migrate_to_ram | 필수 구현 | 선택적 | 해당 없음 |
| PTE 인코딩 | pte_devmap + PRIVATE 마커 | pte_devmap + COHERENT 마커 | pte_devmap |
| GUP (get_user_pages) | 실패 → fault 트리거 | 성공 (직접 접근) | 성공 |
| NUMA 노드 | 별도 NUMA 노드 할당 | 별도 NUMA 노드 할당 | 기존 노드 사용 |
| 커널 등장 | v4.16 | v5.14 | v4.8 |
/* include/linux/memremap.h — ZONE_DEVICE 메모리 타입 정의 */
enum memory_type {
MEMORY_DEVICE_PRIVATE = 1, /* CPU 접근 불가, fault시 마이그레이션 */
MEMORY_DEVICE_COHERENT, /* CPU load/store 가능, 하드웨어 코히어런트 */
MEMORY_DEVICE_FS_DAX, /* DAX 파일시스템 (fsdax) */
MEMORY_DEVICE_GENERIC, /* 범용 DAX (devdax) */
MEMORY_DEVICE_PCI_P2PDMA, /* PCI peer-to-peer DMA */
};
/* dev_pagemap 구조체 — 장치 메모리 매핑 정보 */
struct dev_pagemap {
struct vmem_altmap altmap;
struct percpu_ref ref; /* 참조 카운트 */
struct completion done; /* 해제 완료 대기 */
enum memory_type type; /* PRIVATE/COHERENT/... */
unsigned int flags;
const struct dev_pagemap_ops *ops; /* page_free, migrate_to_ram */
void *owner;
nr_range;
union {
struct range range;
struct range ranges[0];
};
};
코드 설명
MEMORY_DEVICE_PRIVATE: CPU가 직접 접근할 수 없는 장치 메모리. GPU VRAM이 대표적이며, CPU가 이 메모리를 읽으려 하면migrate_to_ram콜백이 호출됩니다.MEMORY_DEVICE_COHERENT: CXL Type2 가속기처럼 CPU가 직접 load/store할 수 있는 장치 메모리. 하드웨어 캐시 코히어런시가 보장됩니다.dev_pagemap.ops: 드라이버가 구현해야 하는 콜백 테이블.page_free(페이지 해제 시),migrate_to_ram(CPU 접근 시 마이그레이션)이 핵심입니다.percpu_ref: 고성능 참조 카운팅으로, 페이지 해제 시점을 안전하게 추적합니다.
MEMORY_DEVICE_COHERENT,
그렇지 않으면 MEMORY_DEVICE_PRIVATE를 선택하세요. 잘못 선택하면 CPU 접근 시 데이터 오염이 발생합니다.
CXL 메모리 문서에서 코히어런시 프로토콜 상세를 확인하세요.
/* PTE 인코딩 차이 — mm/memory.c */
/* PRIVATE: CPU fault → migrate_to_ram 트리거 */
static vm_fault_t handle_device_private_fault(struct vm_fault *vmf) {
struct page *page = vmf->page;
/* PRIVATE 페이지에 CPU가 접근: 시스템 RAM으로 마이그레이션 */
return page->pgmap->ops->migrate_to_ram(vmf);
}
/* COHERENT: GUP 직접 성공, 마이그레이션 불필요 */
/* mm/gup.c — COHERENT 페이지는 GUP에서 바로 반환 */
if (is_device_coherent_page(page)) {
/* 직접 접근 가능 — 마이그레이션 없이 page 반환 */
return page;
}
get_user_pages()는 PRIVATE 페이지에 대해 실패합니다.
이는 sendfile(), splice(), O_DIRECT 등 GUP에 의존하는 I/O 경로에서 장치 메모리 페이지가
먼저 시스템 RAM으로 마이그레이션되어야 함을 의미합니다.
amdgpu vs nouveau HMM 구현 비교
업스트림 Linux에서 HMM을 가장 적극적으로 활용하는 두 GPU 드라이버는 AMD의 amdgpu(KFD)와 NVIDIA의 nouveau입니다. 두 드라이버의 HMM 통합 방식은 GPU 아키텍처 차이로 인해 상당히 다릅니다.
| 항목 | amdgpu (KFD SVM) | nouveau (SVM) |
|---|---|---|
| 핵심 소스 | drivers/gpu/drm/amd/amdkfd/kfd_svm.c | drivers/gpu/drm/nouveau/nouveau_svm.c |
| SVM 구조체 | struct svm_range (VMA별 interval tree) | struct nouveau_svmm (mm 단위) |
| 페이지 테이블 | GPU 전용 VMPT (4/5단계) | GPU 전용 페이지 테이블 (3단계) |
| fault 처리 | XNACK 하드웨어 리트라이 | 소프트웨어 fault → FIFO 스톱 |
| 마이그레이션 | svm_migrate_vma_to_vram() | nouveau_dmem_migrate() |
| notifier | mmu_interval_notifier (range별) | mmu_notifier (mm 전체) |
| Prefetch | SVM_ATTR_PREFETCH_LOC ioctl | 미지원 |
| multi-GPU | XGMI P2P 직접 마이그레이션 | NVLink 없음 (PCIe 경유) |
| Huge Page | 2MB VRAM 매핑 지원 | 4KB 단위만 |
| 활발도 | 매우 활발 (ROCm 상용) | 제한적 (reverse-eng 기반) |
/* amdgpu KFD SVM fault 처리 — drivers/gpu/drm/amd/amdkfd/kfd_svm.c */
int svm_range_restore_pages(struct amdgpu_device *adev,
unsigned int pasid,
uint64_t addr, bool write_fault)
{
struct svm_range *prange;
struct hmm_range hmm_range;
int r;
/* 1. fault 주소로 svm_range 검색 (interval tree) */
prange = svm_range_from_addr(svms, addr, NULL);
/* 2. hmm_range_fault()로 CPU 페이지 테이블 스냅샷 */
r = hmm_range_fault(&hmm_range);
/* 3. GPU 페이지 테이블 갱신 */
r = svm_range_map_to_gpus(prange, ...);
/* 4. XNACK 하드웨어가 자동으로 wavefront 재개 */
return r;
}
/* nouveau SVM fault 처리 — drivers/gpu/drm/nouveau/nouveau_svm.c */
static int nouveau_svm_fault(struct nvif_event *event, void *argv, u32 argc)
{
struct nouveau_svm *svm = container_of(event, ...);
struct hmm_range range;
/* 1. fault 버퍼에서 fault 정보 추출 */
nouveau_svm_fault_buffer_fetch(svm);
/* 2. hmm_range_fault()로 CPU PTE 스냅샷 */
ret = hmm_range_fault(&range);
/* 3. GPU 페이지 테이블 갱신 */
nouveau_range_fault(svmm, ...);
/* 4. FIFO 채널 재시작 — 소프트웨어 복구 */
nouveau_svm_fault_replay(svm);
return NVIF_EVENT_KEEP;
}
migrate_vma 상태 머신
migrate_vma는 CPU 메모리와 장치 메모리 간 페이지 마이그레이션을 수행하는 3단계 API입니다.
각 단계에서 src/dst 배열의 PFN 플래그를 통해 페이지 상태를 추적하며, 부분 실패 시에도 안전하게 롤백(Rollback)할 수 있습니다.
/* mm/migrate_device.c — migrate_vma 3단계 API 사용 패턴 */
/* 1단계: setup — src[] 배열에 원본 PFN 수집 */
struct migrate_vma args = {
.vma = vma,
.start = addr,
.end = addr + size,
.src = src_pfns, /* 출력: 원본 PFN + 플래그 */
.dst = dst_pfns, /* 입력: 대상 PFN + 플래그 */
.pgmap_owner = drv->dev, /* 자기 자신 소유 페이지 필터 */
.flags = MIGRATE_VMA_SELECT_SYSTEM, /* 시스템 RAM 페이지만 */
};
ret = migrate_vma_setup(&args);
/* 2단계: 드라이버가 DMA로 페이지 복사 */
for (i = 0; i < npages; i++) {
if (!(src_pfns[i] & MIGRATE_PFN_MIGRATE))
continue; /* 이동 불가 페이지 스킵 */
/* 장치 메모리에서 페이지 할당 */
dst_pfns[i] = migrate_pfn(alloc_device_page(drv));
dst_pfns[i] |= MIGRATE_PFN_VALID;
/* DMA 엔진으로 src→dst 복사 */
dma_copy(drv, migrate_pfn_to_page(src_pfns[i]),
migrate_pfn_to_page(dst_pfns[i]));
}
/* 3단계: finalize — 성공 페이지의 PTE 교체 */
migrate_vma_pages(&args); /* dst PTE 설치 */
migrate_vma_finalize(&args); /* 원본 page 해제, TLB flush */
코드 설명
migrate_vma_setup(): VMA를 잠그고, 각 페이지의 PTE를 수집하여src[]에 PFN과 플래그를 기록합니다. 이동 가능한 페이지는MIGRATE_PFN_MIGRATE플래그가 설정됩니다.pgmap_owner: 자기 소유 디바이스 페이지를 건너뛰기 위한 필터. GPU-to-GPU 마이그레이션에서 소스와 대상이 같은 장치인 경우를 방지합니다.MIGRATE_VMA_SELECT_SYSTEM: 시스템 RAM 페이지만 선택.MIGRATE_VMA_SELECT_DEVICE_PRIVATE를 쓰면 장치 메모리에서 시스템 RAM으로의 역방향 마이그레이션도 가능합니다.migrate_vma_pages():dst[i]가 유효한 페이지들만 CPU PTE를 새로운 장치 메모리 PTE로 교체합니다.dst[i] = 0인 페이지는 원본 PTE가 복원됩니다.
migrate_vma는 일부 페이지가 실패해도 나머지 페이지는 이동합니다.
드라이버는 반드시 dst[] 배열을 확인하여 실패한 페이지(dst[i] == 0)에 대한 후처리를 해야 합니다.
실패한 장치 페이지는 put_page()로 해제해야 메모리 누수가 없습니다.
ZONE_DEVICE 페이지 생명주기
ZONE_DEVICE 페이지는 일반 시스템 RAM 페이지와 달리 장치 드라이버(Device Driver)가 생성하고 관리합니다.
devm_memremap_pages()로 장치 메모리 영역에 대한 struct page 배열을 할당하며,
이후 드라이버가 제공하는 dev_pagemap_ops 콜백을 통해 생명주기를 제어합니다.
/* ZONE_DEVICE 페이지 등록 — 드라이버 초기화 시 */
static const struct dev_pagemap_ops gpu_pagemap_ops = {
.page_free = gpu_devmem_page_free, /* refcount→0 콜백 */
.migrate_to_ram = gpu_devmem_fault, /* CPU 접근 시 마이그레이션 */
};
int gpu_vram_init(struct gpu_device *gdev)
{
struct dev_pagemap *pgmap;
pgmap = devm_kzalloc(gdev->dev, sizeof(*pgmap), GFP_KERNEL);
pgmap->type = MEMORY_DEVICE_PRIVATE;
pgmap->range.start = gdev->vram_phys_base;
pgmap->range.end = gdev->vram_phys_base + gdev->vram_size - 1;
pgmap->nr_range = 1;
pgmap->ops = &gpu_pagemap_ops;
/* struct page 배열 생성 — vmemmap 영역에 할당 */
gdev->vram_pages = devm_memremap_pages(gdev->dev, pgmap);
if (IS_ERR(gdev->vram_pages))
return PTR_ERR(gdev->vram_pages);
/* 각 page의 zone = ZONE_DEVICE, pgmap 포인터 설정됨 */
return 0;
}
코드 설명
devm_memremap_pages(): 물리 주소 범위에 대해struct page배열을 vmemmap에 할당합니다. 1GB VRAM이면 약 16MB의 struct page 메모리가 필요합니다 (64바이트 * 262,144 페이지).page_free콜백: 페이지 참조 카운트(Reference Count)가 0이 될 때 호출되어, 드라이버의 VRAM 프리리스트로 페이지를 반환합니다.migrate_to_ram콜백: CPU가 PRIVATE 페이지에 접근하면do_swap_page()에서 이 콜백을 호출하여, 장치 메모리에서 시스템 RAM으로 데이터를 복사합니다.
VMEMMAP 영역에서 발생하며, /proc/zoneinfo의 Device 존에서 확인할 수 있습니다.
대형 GPU(80GB A100 등)에서는 1.2GB 이상의 메타데이터가 필요합니다.
mmu_notifier 심층 분석
HMM의 핵심은 CPU 페이지 테이블 변경을 GPU에 동기화하는 것이며, 이를 위해 mmu_notifier 프레임워크를 사용합니다.
v5.10에서 도입된 mmu_interval_notifier는 전체 mm이 아닌 특정 주소 범위만 추적하여 성능을 대폭 개선했습니다.
| 속성 | mmu_notifier (전통) | mmu_interval_notifier (현대) |
|---|---|---|
| 추적 범위 | 전체 mm_struct | 특정 [start, end) 범위 |
| 콜백 | invalidate_range_start/end | invalidate (단일 콜백) |
| 잠금(Lock) | mmap_lock 필요 | interval tree + seq 카운터 |
| 사용자 | nouveau, KVM | amdgpu KFD, HMM core |
| 재시도 | 수동 구현 | seq 카운터 기반 자동 감지 |
| 성능 | 모든 VMA 변경에 호출 | 관심 범위만 호출 |
/* mm/hmm.c — hmm_range_fault()의 seq 카운터 기반 재시도 */
int hmm_range_fault(struct hmm_range *range)
{
struct mmu_interval_notifier *notifier = range->notifier;
unsigned long seq;
int ret;
do {
/* 현재 seq 번호 저장 */
seq = mmu_interval_read_begin(notifier);
range->notifier_seq = seq;
/* CPU 페이지 테이블 워크 */
ret = hmm_vma_walk(range);
if (ret)
return ret;
/* GPU PT 갱신 전 seq 재확인 */
} while (mmu_interval_read_retry(notifier, seq));
/* seq 변경 = 동시 무효화 발생 → 처음부터 재시도 */
return 0;
}
/* mmu_interval_notifier 등록 — 드라이버 초기화 */
static const struct mmu_interval_notifier_ops gpu_notifier_ops = {
.invalidate = gpu_invalidate_range,
};
/* 특정 범위만 추적 등록 */
mmu_interval_notifier_insert(&gpu->notifier, mm,
start, length, &gpu_notifier_ops);
코드 설명
mmu_interval_read_begin(): 현재 무효화(Invalidation) 시퀀스 번호를 반환합니다. 이후 CPU PTE를 읽는 동안 이 번호가 변경되지 않아야 합니다.mmu_interval_read_retry(): 저장한 seq와 현재 seq를 비교합니다. 불일치하면 동시 무효화가 발생한 것이므로 처음부터 재시도합니다.invalidate콜백: CPU 쪽에서munmap(),mprotect(),mremap()등이 PTE를 변경할 때 호출됩니다. GPU 드라이버는 여기서 GPU 페이지 테이블의 해당 매핑을 무효화해야 합니다.- SRCU 보호: invalidate 콜백은 SRCU 읽기 측 임계 영역(Critical Section)에서 호출되므로, 블로킹 연산이 가능합니다 (RCU와 달리 sleep 허용).
invalidate() 콜백 실행 중에 또 다른 무효화가 발생할 수 있습니다.
이 경우 seq 카운터가 2 이상 증가하지만, hmm_range_fault()의 재시도 루프가 이를 자동으로 처리합니다.
드라이버는 중첩을 별도로 관리할 필요가 없습니다.
mmu_interval_notifier는 interval tree를 사용하므로 O(log N + M) 조회입니다 (N=전체 등록 수, M=겹치는 범위 수).
등록된 범위가 수천 개를 넘으면 무효화 콜백 비용이 증가합니다. AMD KFD는 svm_range를 적극적으로 병합하여 이 문제를 완화합니다.
XNACK 리트라이 메커니즘
AMD GPU의 XNACK(eXtra NACK)은 GPU가 유효하지 않은 페이지에 접근했을 때 하드웨어 수준에서 자동 리트라이하는 메커니즘입니다. 이 기능이 HMM 기반 SVM(Shared Virtual Memory)의 핵심 성능 요소입니다.
| 항목 | XNACK ON | XNACK OFF |
|---|---|---|
| SVM 지원 | 완전 지원 | 불가 (사전 매핑만) |
| Oversubscription | 가능 (demand paging) | 불가 |
| Fault 비용 | 해당 wavefront만 정지 | 전체 GPU 작업 중단 |
| ABI 호환성 | XNACK용 바이너리 필요 | 표준 바이너리 |
| 지원 GPU | Vega 이후 (gfx9+) | 모든 AMD GPU |
| 파라미터 | amdgpu.noretry=0 | amdgpu.noretry=1 (기본값) |
| ROCm 사용 | HSA_XNACK=1 | HSA_XNACK=0 |
# XNACK 상태 확인
cat /sys/class/kfd/kfd/topology/nodes/*/properties | grep -i xnack
# XNACK ON으로 amdgpu 로드
modprobe amdgpu noretry=0
# ROCm 런타임에서 XNACK 모드 설정
export HSA_XNACK=1
./rocm_svm_application
--offload-arch=gfx90a:xnack+와 gfx90a:xnack-로 구분하여 빌드해야 합니다.
ROCm 문서의 AMDGCN ISA 섹션을 참고하세요.
HMM 성능 프로파일링(Profiling)
HMM 기반 워크로드의 성능 병목은 주로 페이지 마이그레이션 빈도와 GPU fault 처리 지연에서 발생합니다. 커널 트레이싱 도구와 드라이버별 통계를 조합하여 문제를 진단할 수 있습니다.
# 1. ftrace로 HMM 관련 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/migrate/enable
echo 1 > /sys/kernel/debug/tracing/events/hmm/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 2. bpftrace로 hmm_range_fault 지연 측정
bpftrace -e '
kprobe:hmm_range_fault { @start[tid] = nsecs; }
kretprobe:hmm_range_fault /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 3. bpftrace로 migrate_vma 호출 빈도 추적
bpftrace -e '
kprobe:migrate_vma_setup {
@migrate_count = count();
@migrate_by_comm = count();
}
interval:s:5 { print(@migrate_count); print(@migrate_by_comm); }'
# 4. /proc/vmstat에서 마이그레이션 카운터 확인
grep -E "pgmigrate|thp_migration" /proc/vmstat
# 5. amdgpu debugfs 통계
cat /sys/kernel/debug/dri/0/amdgpu_vm_info
cat /sys/kernel/debug/dri/0/amdgpu_gpu_recover
| 도구 | 측정 항목 | 사용 시나리오 |
|---|---|---|
ftrace | hmm/migrate 이벤트 추적 | 마이그레이션 경로 디버깅 |
bpftrace | hmm_range_fault 지연 히스토그램 | fault 처리 지연 분석 |
/proc/vmstat | pgmigrate_success/fail 카운터 | 마이그레이션 성공률 모니터링 |
amdgpu debugfs | VM 정보, GPU 복구 이력 | GPU 드라이버 문제 진단 |
perf stat | dTLB/iTLB 미스 | TLB 압박으로 인한 fault 빈도 파악 |
rocm-smi | VRAM 사용량, 온도, 처리량(Throughput) | GPU 메모리 활용도 확인 |
/* 드라이버에서 마이그레이션 대역폭 측정 패턴 */
static void measure_migration_bw(struct gpu_device *gdev,
unsigned long npages)
{
ktime_t start, end;
u64 elapsed_ns, bw_mbps;
start = ktime_get();
/* migrate_vma 수행 */
do_migrate_to_vram(gdev, npages);
end = ktime_get();
elapsed_ns = ktime_to_ns(ktime_sub(end, start));
bw_mbps = (npages * PAGE_SIZE * 1000ULL) / elapsed_ns;
dev_info(gdev->dev, "Migration: %lu pages in %llu us = %llu MB/s\n",
npages, elapsed_ns / 1000, bw_mbps);
}
대형 페이지(Huge Page) 마이그레이션
HMM은 2MB(PMD 레벨) 대형 페이지의 직접 마이그레이션을 지원합니다. THP(Transparent Huge Pages)를 분할(split)하지 않고 통째로 GPU VRAM으로 이동하면, TLB 엔트리 수 감소와 DMA 전송 효율 향상이라는 두 가지 이점을 얻습니다.
/* mm/migrate_device.c — THP 직접 마이그레이션 지원 */
/* migrate_vma_collect_pmd() — PMD 레벨 compound page 처리 */
static int migrate_vma_collect_pmd(pmd_t *pmdp, ...)
{
if (pmd_trans_huge(*pmdp)) {
/* THP 발견 — 분할 없이 직접 수집 */
struct page *page = pmd_page(*pmdp);
unsigned long pfn = page_to_pfn(page);
for (i = 0; i < HPAGE_PMD_NR; i++) {
src_pfns[i] = migrate_pfn(pfn + i) |
MIGRATE_PFN_VALID | MIGRATE_PFN_MIGRATE;
}
/* PMD를 통째로 언맵 */
pmdp_invalidate(vma, addr, pmdp);
return 0;
}
/* 일반 4KB PTE 수집 경로 */
...
}
| 항목 | 4KB 페이지 | 2MB Huge Page |
|---|---|---|
| TLB 엔트리 수 (2MB 영역) | 512개 | 1개 |
| DMA 디스크립터 | 512개 (scatter-gather) | 1개 (연속 전송) |
| PTE 갱신 횟수 | 512회 | 1회 (PMD) |
| 마이그레이션 지연 | 높음 (오버헤드 지배적) | 낮음 (데이터 전송 지배적) |
| GPU 페이지 테이블 | PTE 레벨 | PDE 레벨 (2MB 매핑) |
| 제약 | 없음 | 연속 물리 메모리 필요, 부분 마이그레이션 불가 |
svm_range가 2MB 정렬되고 전체가 동일 NUMA 노드에 있으면
자동으로 2MB GPU PDE 매핑을 생성합니다. 이는 AI/ML 워크로드에서 대용량 텐서 접근 시 GPU TLB 미스를 대폭 줄입니다.
Huge Pages 문서에서 THP 설정 방법을 확인하세요.
MADV_NOHUGEPAGE로 특정 VMA에서 THP를 비활성화할 수 있습니다.
Intel Xe GPU HMM 통합
Intel의 차세대 GPU 드라이버 Xe는 기존 i915를 대체하며, HMM 기반 SVM(Shared Virtual Memory)을 핵심 기능으로 지원합니다.
Xe는 xe_vm과 xe_bo(Buffer Object) 구조를 사용하며, EU(Execution Unit)의 하드웨어 페이지 폴트를 통해
demand paging을 구현합니다.
| 항목 | Intel Xe | Intel i915 |
|---|---|---|
| HMM 지원 | 네이티브 SVM | 미지원 (GEM 기반) |
| VM 구조 | xe_vm (HMM 통합) | i915_address_space |
| Buffer Object | xe_bo (TTM 기반) | i915_gem_object |
| GPU 페이지 테이블 | PPGTT 4레벨 | PPGTT 4레벨 |
| Page Fault | GuC 매개 HW fault | 미지원 |
| Local Memory | LMEM (DEVICE_PRIVATE) | LMEM (GEM 관리) |
| 스케줄러(Scheduler) | GuC 기반 | execlists / GuC |
| 상태 | v6.8+ 업스트림 | 유지보수 모드 |
/* drivers/gpu/drm/xe/xe_svm.c — Xe SVM fault 처리 */
static int xe_svm_fault(struct xe_vm *vm, struct xe_vma *vma,
u64 fault_addr, bool is_write)
{
struct hmm_range range = {
.notifier = &vma->notifier,
.start = fault_addr & PAGE_MASK,
.end = (fault_addr & PAGE_MASK) + PAGE_SIZE,
.hmm_pfns = pfns,
.default_flags = HMM_PFN_REQ_FAULT,
};
if (is_write)
range.default_flags |= HMM_PFN_REQ_WRITE;
/* HMM을 통해 CPU PTE 스냅샷 */
ret = hmm_range_fault(&range);
if (ret)
return ret;
/* PPGTT에 매핑 설치 */
ret = xe_pt_update(vm, vma, pfns, ...);
return ret;
}
코드 설명
xe_vm: Xe 드라이버의 VM(가상 메모리) 관리 구조체. HMM의mmu_interval_notifier를 직접 내장합니다.xe_bo: Xe의 Buffer Object로, TTM(Translation Table Manager) 위에 구축됩니다. LMEM(Local Memory)과 시스템 RAM 간 이동을 관리합니다.GuC: GPU의 마이크로컨트롤러로, EU에서 발생한 페이지 폴트를 커널 드라이버(KMD)에 전달하는 중간 매개체입니다.PPGTT: Per-Process Graphics Translation Table. 프로세스별 GPU 페이지 테이블로, CPU의 CR3에 해당합니다.
zeMemAllocShared()로 CPU-GPU 공유 메모리를 할당합니다. 내부적으로 HMM의 hmm_range_fault()와
migrate_vma를 사용합니다. GPU 서브시스템 문서에서 DRM/KMS 전체 구조를 확인하세요.
CXL Type2 HMM 연동
CXL(Compute Express Link) Type2 디바이스는 가속기(GPU, FPGA, AI 칩)에 부착된 메모리를 호스트 CPU와 공유하는 장치입니다.
HMM의 MEMORY_DEVICE_COHERENT 타입을 사용하여 CPU가 직접 load/store할 수 있는 장치 메모리를 관리하며,
CXL.cache/CXL.mem 프로토콜로 하드웨어 코히어런시를 보장합니다.
| CXL 타입 | 장치 예시 | ZONE_DEVICE 타입 | HMM 활용 |
|---|---|---|---|
| Type1 | SmartNIC, 네트워크 가속기 | 해당 없음 | CXL.cache만 (메모리 없음) |
| Type2 | GPU, FPGA, AI 가속기 | MEMORY_DEVICE_COHERENT | HMM 핵심 대상 (SVM) |
| Type3 | CXL 메모리 확장기 | MEMORY_DEVICE_GENERIC / DAX | NUMA 노드로 관리 |
/* CXL Type2 드라이버에서 DEVICE_COHERENT 등록 */
static int cxl_type2_mem_init(struct cxl_dev *cxldev)
{
struct dev_pagemap *pgmap;
pgmap = devm_kzalloc(&cxldev->dev, sizeof(*pgmap), GFP_KERNEL);
pgmap->type = MEMORY_DEVICE_COHERENT; /* CPU 직접 접근 가능 */
pgmap->range.start = cxldev->mem_base;
pgmap->range.end = cxldev->mem_base + cxldev->mem_size - 1;
pgmap->ops = &cxl_type2_pagemap_ops;
/* NUMA 노드 등록 — 별도 메모리 티어 */
pgmap->nr_range = 1;
/* HDM 디코더 설정 — 물리 주소 범위 매핑 */
cxl_hdm_decoder_setup(cxldev, pgmap->range.start,
pgmap->range.end - pgmap->range.start + 1);
/* struct page 배열 생성 */
cxldev->pages = devm_memremap_pages(&cxldev->dev, pgmap);
if (IS_ERR(cxldev->pages))
return PTR_ERR(cxldev->pages);
/* NUMA 메모리 티어에 등록 (demotion 대상) */
register_memory_tier(cxldev->numa_node, MEMORY_TIER_DEFAULT_RANK - 1);
return 0;
}
코드 설명
MEMORY_DEVICE_COHERENT: CXL Type2 메모리는 CXL.cache 프로토콜로 CPU 캐시 코히어런시가 보장되므로, CPU가 직접 load/store할 수 있습니다.HDM 디코더: Host-managed Device Memory 디코더로, CXL 장치의 물리 주소 범위를 호스트의 물리 주소 공간에 매핑합니다.register_memory_tier(): CXL 메모리를 NUMA 메모리 티어에 등록합니다. DDR보다 낮은 랭크를 설정하면 demotion 대상이 됩니다.- Bias 모드: Host-Bias에서는 CPU가 캐시 라인(Cache Line) 소유권을 가지고, Device-Bias에서는 가속기가 소유권을 가집니다. 워크로드에 따라 동적으로 전환하여 스누프 트래픽을 최소화합니다.
MEMORY_DEVICE_COHERENT를 사용해야 합니다.
만약 장치가 CXL.cache를 지원하지 않는다면(예: PCIe 전용 GPU), MEMORY_DEVICE_PRIVATE를 사용하고
migrate_to_ram 콜백을 구현해야 합니다. 두 타입을 혼동하면 데이터 손상이 발생합니다.
COW/fork()/exec과 HMM
fork() 시스템 콜은 부모 프로세스의 페이지 테이블(Page Table)을 자식에게 복사합니다.
device_private PTE는 스왑(Swap) 유사 엔트리로 인코딩되어 있으므로,
copy_page_range()가 이를 그대로 자식 페이지 테이블에 복제합니다.
이후 자식(또는 부모)이 해당 주소에 접근하면 migrate_to_ram이 호출되어
GPU에서 CPU로 페이지가 복귀하고, 이어서 COW(Copy-on-Write) 분리가 발생합니다.
device_private PTE는 커널 내부에서 스왑 엔트리와 유사한 형식으로 인코딩됩니다.
fork() 시 copy_page_range() → copy_nonpresent_pte() 경로에서
이 엔트리를 그대로 자식 프로세스의 페이지 테이블에 복사합니다.
이때 GPU 드라이버에는 mmu_notifier를 통해 dup_mmap() 이벤트가 통지됩니다.
자식 프로세스(또는 fork 이후 부모)가 해당 가상 주소에 접근하면 다음 순서로 처리됩니다:
- CPU 페이지 폴트 발생 → swap-like PTE 감지
do_swap_page()→device_private타입 확인migrate_to_ram()콜백 호출 → GPU에서 CPU로 DMA 전송- 페이지가 CPU에 도착 → COW 처리 (부모/자식 독립 복사본 생성)
exec()의 경우에는 기존 mm_struct를 완전히 해체하므로,
모든 device_private 엔트리가 폐기되고
mmu_notifier_release()를 통해 GPU 드라이버가 VRAM 자원을 정리합니다.
커널 경로 코드
/* mm/memory.c — fork() 시 device_private PTE 처리 */
static inline int
copy_nonpresent_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte,
struct vm_area_struct *src_vma,
unsigned long addr, int *rss)
{
swp_entry_t entry = pte_to_swp_entry(pte);
if (is_device_private_entry(entry)) {
struct page *page = pfn_swap_entry_to_page(entry);
/*
* device_private 페이지의 참조 카운트 증가.
* fork 시 부모와 자식 모두 같은 device page를 참조.
* 나중에 접근 시 migrate_to_ram → COW 순서로 분리됨.
*/
get_page(page);
rss[MM_ANONPAGES]++;
/* 자식 PTE에 동일한 device_private 엔트리 설정 */
set_pte_at(dst_mm, addr, dst_pte, pte);
return 0;
}
/* ... 다른 swap 엔트리 처리 ... */
}
코드 설명
is_device_private_entry(): PTE가 device_private 스왑 엔트리인지 확인합니다.get_page(): 참조 카운트를 증가시켜 부모와 자식이 동일한 GPU 페이지를 공유합니다.rss[MM_ANONPAGES]++: device_private 페이지도 익명 페이지 RSS에 카운트됩니다.- 실제 데이터 분리는 접근 시점까지 지연(Lazy)되어,
migrate_to_ram+ COW로 처리됩니다.
fork()를 호출하면,
자식 프로세스의 모든 메모리 접근이 migrate_to_ram을 트리거합니다.
수 GB의 GPU 메모리가 PCIe를 통해 CPU로 역류하면서 심각한 성능 저하가 발생할 수 있습니다.
GPU 워크로드가 활성화된 상태에서 fork()를 피하거나,
fork() 직후 즉시 exec()를 호출하여 mm을 교체하는 것이 권장됩니다.
Python multiprocessing의 fork 방식 대신 spawn 또는 forkserver 방식을 사용하세요.
Swap/OOM과 HMM 상호작용
device_private 페이지는 이미 GPU VRAM에 존재하므로 디스크(Disk)로의 스왑 아웃 대상이 아닙니다.
커널 관점에서 이 페이지들은 "이미 스왑된" 상태와 유사합니다.
그러나 OOM Killer와 메모리 압박(Memory Pressure) 상황에서
device_private 페이지가 프로세스의 RSS에 포함되어 있으므로,
이들의 처리 방식을 정확히 이해하는 것이 중요합니다.
| 동작 | 일반 익명 페이지 | device_private 페이지 |
|---|---|---|
| Swap Out | 디스크/zswap으로 기록 | 불가 — 이미 GPU에 있음 (스왑 불필요) |
| OOM Kill | 프로세스 종료, 페이지 해제 | mmu_notifier_release → GPU 드라이버 정리 |
| OOM Reaper | 페이지 테이블 해체, 페이지 해제 | device_private PTE 건너뜀 (GPU 드라이버가 정리) |
| RSS 계산 | MM_ANONPAGES에 포함 | MM_ANONPAGES에 포함 (동일 카운터) |
| mlock | 스왑 아웃 방지, RAM에 고정 | 효과 없음 — device_private는 스왑 대상 아님 |
| madvise(DONTNEED) | 즉시 해제 | device_private PTE 해제 + mmu_notifier 통지 |
| madvise(PAGEOUT) | 스왑 아웃 힌트 | 무시됨 — reclaim 대상 아님 |
| NUMA balancing | NUMA 힌트 폴트로 마이그레이션 | device_private에는 적용 안 됨 |
OOM Reaper와 device_private
OOM Killer가 프로세스를 선택하면, OOM Reaper가 해당 프로세스의 페이지 테이블을 순회하며
가능한 한 빨리 메모리를 회수합니다. device_private PTE를 만나면
직접 해제하지 않고 건너뜁니다 — GPU 드라이버만이 VRAM 페이지를 안전하게 해제할 수 있기 때문입니다.
/* mm/oom_kill.c — OOM reaper의 device_private 처리 */
static bool __oom_reap_task_mm(struct mm_struct *mm)
{
struct vm_area_struct *vma;
bool ret = true;
/*
* mmu_notifier_release()가 먼저 호출되어
* GPU 드라이버에게 정리 기회를 줌
*/
mmu_notifier_release(mm);
VMA_ITERATOR(vmi, mm, 0);
for_each_vma(vmi, vma) {
if (vma->vm_flags & (VM_HUGETLB | VM_PFNMAP))
continue;
/*
* unmap_page_range()가 페이지 테이블 순회.
* device_private PTE는 zap_pte_range()에서
* 참조 카운트를 감소시키고 PTE를 클리어.
* 실제 VRAM 해제는 page_free 콜백에서 처리.
*/
unmap_page_range(&tlb, vma, vma->vm_start,
vma->vm_end, &details);
}
return ret;
}
코드 설명
mmu_notifier_release(): OOM 과정에서 가장 먼저 호출됩니다. GPU 드라이버는 이 콜백에서 GPU 페이지 테이블을 무효화하고, 진행 중인 GPU 작업을 중단합니다.zap_pte_range(): device_private PTE를 만나면pfn_swap_entry_to_page()로 page를 찾고,put_page()로 참조 카운트를 감소시킵니다.- 참조 카운트가 0이 되면
dev_pagemap_ops.page_free()콜백이 호출되어 GPU 드라이버가 VRAM 페이지를 자유 풀에 반환합니다.
hmm_range_fault()로 GPU 페이지 테이블에 매핑된 CPU 페이지가 스왑 아웃 대상이 되면,
커널은 먼저 mmu_notifier invalidate 콜백을 호출합니다.
GPU 드라이버는 이 콜백에서 해당 범위의 GPU 페이지 테이블 엔트리를 무효화하고,
다음 GPU 접근 시 hmm_range_fault()를 다시 호출하여 최신 매핑을 얻어야 합니다.
mlock()은 페이지를 RAM에 고정하여 스왑 아웃을 방지하는 시스템 콜입니다.
그러나 device_private 페이지는 애초에 스왑 대상이 아니므로
mlock()이 아무 효과가 없습니다.
GPU 메모리를 "고정"하려면 GPU 드라이버의 BO(Buffer Object) 핀(Pin) API를 사용해야 합니다.
HMM 메모리 일관성 모델 (Memory Ordering)
HMM은 CPU와 GPU 사이에 최종 일관성(Eventual Consistency) 모델을 제공합니다.
하드웨어 캐시 코히어런시(Cache Coherency)가 아닌
소프트웨어 기반 동기화(mmu_notifier + migrate_vma)로 일관성을 유지합니다.
MEMORY_DEVICE_COHERENT(CXL)만이 하드웨어 캐시 코히어런시를 제공합니다.
| 특성 | PRIVATE (GPU VRAM) | COHERENT (CXL Type2) |
|---|---|---|
| 일관성 모델 | 최종 일관성 (Eventual Consistency) | 캐시 코히어런시 (Cache Coherency) |
| CPU 쓰기 → GPU 가시성 | mmu_notifier invalidate + GPU re-fault 필요 | 하드웨어 스누프로 즉시 반영 |
| GPU 쓰기 → CPU 가시성 | migrate_to_ram (DMA 전송) 필요 | 하드웨어 스누프로 즉시 반영 |
| 동기화 오버헤드 | 높음 (마이그레이션 + TLB flush) | 낮음 (캐시 라인 수준 스누프) |
| 대역폭 | PCIe/NVLink DMA 속도 | CXL.cache 속도 (CPU 캐시 레이턴시) |
| 원자성 보장 | 시퀀스 번호 (mmu_interval_read_begin/retry) | 하드웨어 원자적 연산 가능 |
MEMORY_DEVICE_PRIVATE 모드에서 GPU가 데이터를 쓴 직후부터
migrate_to_ram이 완료될 때까지 CPU는 최신 데이터를 볼 수 없습니다.
GPU 드라이버는 migrate_to_ram 구현에서 반드시 GPU DMA가 완전히 완료된 후에
함수를 반환해야 합니다 (GPU fence/barrier 사용).
이 규칙을 어기면 CPU가 불완전한 데이터를 읽는 데이터 손상(Data Corruption)이 발생합니다.
hmm_range_fault 내부 처리 상세
hmm_range_fault()는 GPU 드라이버가 CPU 페이지 테이블의 현재 상태를
GPU 페이지 테이블에 반영하기 위해 호출하는 핵심 함수입니다.
내부적으로 복잡한 페이지 테이블 워크(Page Table Walk)와 폴트 처리를 수행합니다.
hmm_range_fault()의 핵심 내부 콜백은 hmm_vma_walk_pmd()입니다.
이 함수가 PMD 단위로 페이지 테이블을 순회하며 각 PTE의 상태에 따라 적절한 처리를 수행합니다.
/* mm/hmm.c — hmm_vma_walk_pmd 내부 로직 (간략화) */
static int hmm_vma_walk_pmd(pmd_t *pmdp, unsigned long start,
unsigned long end, struct mm_walk *walk)
{
struct hmm_vma_walk *hmm_vma_walk = walk->private;
struct hmm_range *range = hmm_vma_walk->range;
unsigned long *hmm_pfns = range->hmm_pfns;
pte_t *ptep, pte;
spinlock_t *ptl;
ptep = pte_offset_map_lock(walk->mm, pmdp, start, &ptl);
for (; start < end; start += PAGE_SIZE, ptep++, hmm_pfns++) {
pte = ptep_get(ptep);
if (pte_none(pte)) {
/* 매핑 없음 — REQ_FAULT이면 demand fault 발생 */
if (*hmm_pfns & HMM_PFN_REQ_FAULT) {
pte_unmap_unlock(ptep, ptl);
return hmm_vma_fault(start, end, walk);
}
*hmm_pfns = 0; /* HMM_PFN_NONE */
continue;
}
if (!pte_present(pte)) {
swp_entry_t entry = pte_to_swp_entry(pte);
if (is_device_private_entry(entry)) {
/* device_private → PFN 수집, GPU가 직접 접근 가능 */
*hmm_pfns = pfn_swap_entry_to_page(entry)
| HMM_PFN_VALID;
if (is_writable_device_private_entry(entry))
*hmm_pfns |= HMM_PFN_WRITE;
continue;
}
/* 일반 swap — REQ_FAULT이면 swap-in 트리거 */
if (*hmm_pfns & HMM_PFN_REQ_FAULT) {
pte_unmap_unlock(ptep, ptl);
return hmm_vma_fault(start, end, walk);
}
*hmm_pfns = 0;
continue;
}
/* present PTE — read-only인데 WRITE 요청이면 COW */
if ((*hmm_pfns & HMM_PFN_REQ_WRITE) && !pte_write(pte)) {
pte_unmap_unlock(ptep, ptl);
return hmm_vma_fault(start, end, walk);
}
/* PFN과 플래그 수집 */
*hmm_pfns = pte_pfn(pte) | HMM_PFN_VALID;
if (pte_write(pte))
*hmm_pfns |= HMM_PFN_WRITE;
}
pte_unmap_unlock(ptep, ptl);
return 0;
}
코드 설명
pte_offset_map_lock(): PMD 내의 PTE 배열에 대한 포인터를 얻고 페이지 테이블 락을 획득합니다.pte_none(): PTE가 비어 있으면(아직 매핑되지 않은 주소),HMM_PFN_REQ_FAULT플래그가 설정된 경우에만 demand paging을 트리거합니다.is_device_private_entry(): 이미 GPU에 있는 페이지이므로 해당 PFN을 직접 수집합니다. GPU 드라이버는 이 PFN이 자신의 VRAM 주소임을 알고 있습니다.hmm_vma_fault(): 내부에서handle_mm_fault()를 호출하여 demand allocation이나 COW를 트리거합니다.HMM_PFN_VALID | HMM_PFN_WRITE: GPU 드라이버가 이 플래그를 확인하여 GPU 페이지 테이블에 읽기/쓰기 권한을 적절히 설정합니다.
hmm_range_fault()는 mmap_read_lock을 획득하므로
호출 횟수를 최소화하는 것이 중요합니다. GPU 드라이버는 가능한 한 넓은 범위를
한 번의 hmm_range_fault() 호출로 처리하여 락 오버헤드를 분산시켜야 합니다.
AMD amdgpu 드라이버의 경우 최대 512MB 범위를 한 번에 처리합니다.
HMM과 GUP/pin_user_pages 상호작용
get_user_pages()(GUP)는 사용자 공간 메모리 페이지의 물리 주소를 얻어
커널이나 DMA가 직접 접근할 수 있게 하는 함수입니다.
그러나 device_private 페이지는 CPU가 직접 접근할 수 없으므로,
GUP는 이 페이지를 먼저 CPU로 마이그레이션한 후에야 핀(Pin)할 수 있습니다.
GUP가 device_private 페이지를 만나는 주요 경로는 다음과 같습니다:
- GUP-fast (락 없는 빠른 경로): PTE가 present가 아니므로 즉시 실패하고 느린 경로로 전환
- GUP-slow:
faultin_page()→handle_mm_fault()→do_swap_page() do_swap_page()에서device_private감지 →migrate_to_ram()호출- GPU에서 CPU로 DMA 전송 완료 후 일반 CPU 페이지로 변환
- GUP가 해당 CPU 페이지를 핀(참조 카운트 증가)
| ZONE_DEVICE 유형 | GUP-fast | GUP-slow | 동작 |
|---|---|---|---|
MEMORY_DEVICE_PRIVATE |
실패 | 마이그레이션 후 핀 | migrate_to_ram → CPU 페이지 핀 |
MEMORY_DEVICE_COHERENT |
성공 가능 | 성공 | 마이그레이션 없이 직접 핀 (CPU 접근 가능) |
MEMORY_DEVICE_FS_DAX |
성공 가능 | 성공 | LONGTERM 핀 시 추가 제약 있음 |
MEMORY_DEVICE_GENERIC |
실패 | 드라이버 의존 | 일반적으로 DMA 전용 |
ibv_reg_mr()로 메모리를 등록하면 해당 페이지가 CPU RAM에 장기 핀됩니다.
GPU가 이 범위를 hmm_range_fault()로 접근하면 해당 페이지가 GPU로 마이그레이션되지만,
RDMA 핀이 유지되므로 실제로는 마이그레이션이 실패하거나,
마이그레이션 후 RDMA 측에서 다시 CPU로 끌어와 핀합니다.
이 순환이 반복되면 성능이 심각하게 저하됩니다.
GPU 워크로드와 RDMA 버퍼는 가급적 다른 가상 주소 범위를 사용하세요.
HMM 페이지 상태 전이도
HMM에서 관리하는 페이지는 다양한 상태 사이를 전이합니다. 아래 다이어그램은 하나의 가상 주소에 대응하는 페이지가 거칠 수 있는 모든 상태와 전이 조건을 보여줍니다.
위 상태 전이도에서 핵심 전이는 다음과 같습니다:
| 전이 | 트리거 | 필요한 잠금 | mmu_notifier |
|---|---|---|---|
| 미할당 → CPU 익명 | 첫 접근 (demand fault) | mmap_read_lock | 호출 안 됨 |
| CPU 익명 → Device Private | GPU 폴트 / migrate_vma_setup | mmap_read_lock + page lock | invalidate_range |
| Device Private → CPU 익명 | CPU 접근 / migrate_to_ram | mmap_read_lock + page lock | 호출 안 됨 (CPU 측) |
| CPU 익명 → 스왑됨 | 메모리 압박 (reclaim) | page lock + swap_lock | invalidate_range (GPU 매핑 시) |
| 스왑됨 → CPU 익명 | CPU 접근 (swap in) | mmap_read_lock | 호출 안 됨 |
| CPU 익명 → GUP 핀됨 | O_DIRECT, RDMA 등 | page lock + refcount | 해당 없음 |
| Device Private → GUP 핀됨 | GUP slow (migrate_to_ram 후 핀) | mmap_read_lock + page lock | migrate_to_ram 중 GPU 통지 |
| any → 해제됨 | munmap / exit / OOM | mmap_write_lock | release (exit 시) |
| CPU/Device → COW 복사본 | fork + 접근 | mmap_read_lock + page lock | dup_mmap (fork 시) |
실전 지연 시간 참고 데이터
HMM 기반 시스템의 성능을 이해하려면 각 인터커넥트(Interconnect)의 지연 시간과 대역폭, 그리고 HMM API 자체의 소프트웨어 오버헤드를 파악해야 합니다. 아래 데이터는 실제 시스템 측정과 하드웨어 사양서를 기반으로 한 참고 수치입니다.
| 항목 | 4KB 페이지 | 2MB 대형 페이지 | 비고 |
|---|---|---|---|
| PCIe 4.0 x16 DMA | ~2-5 μs | ~60-80 μs | 단방향 대역폭 ~25 GB/s |
| PCIe 5.0 x16 DMA | ~1.5-4 μs | ~35-50 μs | 단방향 대역폭 ~50 GB/s |
| PCIe 6.0 x16 DMA | ~1-3 μs | ~20-35 μs | 단방향 대역폭 ~121 GB/s (PAM4) |
| NVLink 3.0 (A100) | ~0.5-1.5 μs | ~10-20 μs | 양방향 대역폭 ~600 GB/s |
| NVLink 4.0 (H100) | ~0.3-1 μs | ~7-15 μs | 양방향 대역폭 ~900 GB/s |
| CXL 2.0 | ~150-250 ns | ~2-5 μs | 캐시 코히어런트, ~32 GB/s |
| CXL 3.0 | ~80-150 ns | ~1-3 μs | 캐시 코히어런트, ~64 GB/s |
| HMM 소프트웨어 오버헤드 | 일반적인 지연 시간 | 설명 |
|---|---|---|
| hmm_range_fault (캐시 히트) | ~1-5 μs | 마이그레이션 불필요, 페이지 테이블 워크만 |
| hmm_range_fault (폴트 필요) | ~10-50 μs | handle_mm_fault 포함, 페이지 할당 |
| migrate_vma_setup + finalize (4KB) | ~5-20 μs + DMA 시간 | 소프트웨어 설정 + 실제 DMA 전송 |
| migrate_vma_setup + finalize (2MB) | ~10-30 μs + DMA 시간 | THP 마이그레이션, 512회 PTE 업데이트 |
| mmu_notifier invalidate | ~0.5-3 μs | 콜백 체인 순회 + 드라이버 처리 |
| GPU TLB flush | ~1-10 μs | GPU 아키텍처 의존, CU/SM 수에 비례 |
| mmap_read_lock 경합 시 | ~10-1000 μs | 스레드 수에 따라 급증, per-VMA lock으로 완화 |
perf stat과 GPU 프로파일러(rocprof, nsys)를 병행하여 측정하세요.
- 대형 페이지 사용: 4KB 페이지 512개보다 2MB 대형 페이지 1개를 마이그레이션하는 것이 DMA 설정 오버헤드를 1/512로 줄입니다.
- 배치 마이그레이션:
migrate_vma_setup()에서 가능한 한 넓은 범위를 한 번에 처리하세요. - 선제적 마이그레이션: GPU 커널 실행 전에 필요한 데이터를 미리 마이그레이션하여 GPU 폴트 지연을 제거하세요.
- CXL 활용: CXL Type2를 사용하면 마이그레이션 없이 CPU/GPU 모두 접근 가능하여, 접근 패턴이 불규칙한 워크로드에 유리합니다.
참고자료
커널 문서
- Kernel Documentation: HMM — HMM 설계 문서
- Kernel Documentation: DRM Memory Management — HMM을 활용하는 DRM 메모리 관리
LWN 기사
- LWN: Device-accessible page faults (2016) — 디바이스 페이지 폴트 처리 개념
- LWN: Heterogeneous memory management (2017) — HMM 설계 및 도입 배경
- LWN: HMM after a year in mainline (2019) — HMM 메인라인 편입 1년 후 현황
커널 소스
- mm/hmm.c — HMM 핵심 구현
- include/linux/hmm.h — HMM API 헤더
- mm/migrate_device.c — 디바이스 메모리 마이그레이션 구현
- lib/test_hmm.c — HMM 테스트 드라이버
관련 문서
- GPU 서브시스템 (DRM/KMS) — HMM을 활용하는 GPU 드라이버 전체 구조
- NPU (Neural Processing Unit) — AI 가속기와 DMA-BUF/HMM 통합
- CXL 메모리 — ZONE_DEVICE Coherent를 사용하는 CXL Type2/3
- 고급 메모리 관리 — 페이지 마이그레이션, NUMA 티어링 기반
- NUMA — NUMA 노드와 메모리 티어 정책
- DMA — DMA-BUF 버퍼 공유 메커니즘
- MMU & TLB — HMM이 동기화하는 CPU 페이지 테이블 구조
- Huge Pages — HMM 대형 페이지 마이그레이션의 기반
- VMA / mmap — HMM이 조회하는 가상 메모리 영역 구조
- IOMMU — GPU/NPU의 DMA 주소 변환(Address Translation) 및 보안 격리(Isolation)
- ROCm/HIP — AMD GPU XNACK, SVM, KFD 드라이버
- Spinlock — mmu_notifier 경로의 잠금 메커니즘