GPU가 노는데 왜 느릴까 — CUDA 스케줄링과 TensorFlow 메모리 관리 최적화

딥러닝 워크로드 병목의 실제 원인인 CUDA 스트림·MPS·MIG 스케줄링과 TensorFlow BFC·XLA·tf.data 메모리 관리를 OS/커널 기능과 연동해 최적화하는 방법을 코드로 정리한다.

2026-08-12 · 최초 발행 2025-12-11

GPU 사용률 모니터링 대시보드는 낮게 나오는데 학습·추론은 계속 느리다면, 문제는 GPU 연산 자체가 아니라 그 앞뒤에 있는 스케줄러와 메모리 관리일 가능성이 높다. 딥러닝 워크로드의 지배적 병목은 CUDA 레벨의 커널 스케줄링과 호스트 메모리·페이지 관리에서 자주 발생한다. CUDA 스케줄링과 TensorFlow 런타임의 메모리 할당 전략을 OS/커널 기능과 연동해 시스템 관점에서 최적화하는 절차가 필요한 이유다.

어디서 병목이 생기는가

GPU 스케줄링은 CUDA 스트림, 우선순위, CTA/블록 단위 디스패치, SM(Streaming Multiprocessor) 점유율 제어 메커니즘을 총칭한다. 단일 GPU 안에서 여러 커널이 동시에 실행될 수 있는지, 선점이 가능한지, QoS가 보장되는지를 이 메커니즘이 결정한다. 시스템 차원에서는 MPS(Multi-Process Service), MIG(Multi-Instance GPU), 컨텍스트 스위칭, 타임슬라이스 정책까지 포함한다.

OS 메모리 관리와 딥러닝 런타임의 접점도 별개의 문제다. TensorFlow의 BFC(Best-Fit with Coalescing) GPU 메모리 할당기, 호스트 메모리 풀링, 그래프/커널 캐시가 OS의 VMM, THP/HugePages, NUMA 정책과 상호작용한다. UVM(Unified Virtual Memory)과 페이지 폴트 경로에서는 CUDA 드라이버와 커널의 DMA 전송이 관여하고, 컨테이너 환경에서는 cgroups(v1/v2) 메모리 제한과 OOM Killer의 영향도 고려해야 한다.

이 두 층을 함께 다루는 목표는 GPU·CPU·메모리의 파이프라인 병목을 제거하고, p99 지연시간의 변동성을 완화하며, 노이즈 네이버를 격리하고, 비용 대비 처리량을 극대화하는 것이다.

CUDA 스케줄링 메커니즘

스트림과 우선순위는 cudaStreamCreateWithPriority로 다중 스트림을 구성해 대기열 간 선호도를 부여한다. 우선순위는 대기 중인 블록의 디스패치 순서에 영향을 주지만, 실행 중인 블록을 강제로 선점하는 것은 제한적이다.

동시 커널 실행과 SM 점유율은 커널 블록 크기, 레지스터·공유메모리 사용량에 따라 Occupancy 최적화가 필요하다. 블록 크기를 조정하면 SM 간 공정성과 파이프라인 활용도를 개선할 수 있다.

MPS는 여러 프로세스의 커널을 단일 서버 프로세스가 병합 스케줄링해 컨텍스트 스위칭 오버헤드를 줄인다. MIG는 물리 GPU를 하드 파티션해 더 강한 격리를 제공한다.

TensorFlow 메모리 할당과 최적화

BFC Allocator는 기본 고정 할당 대비 메모리 파편화와 예약 이슈가 있다. set_memory_growth로 필요할 때만 메모리를 늘리거나, virtual device configuration으로 상한을 설정할 수 있다.

XLA는 연산 융합과 버퍼 라이프타임 최적화로 메모리 풋프린트와 커널 호출 수를 줄인다. 그래프를 정적화할 수 있는 워크로드에서 효과가 크다.

tf.data 파이프라인에서는 prefetch, parallel map, interleave, prefetch_to_device로 GPU 대기 시간을 줄인다. 핀드 메모리를 사용하면 H2D 전송 지연도 줄어든다.

OS·커널 연계

NUMA/CPU 바인딩에서는 GPU가 연결된 PCIe 루트와 동일한 NUMA 노드로 CPU·메모리를 바인딩해 원격 메모리 접근 비용을 줄인다. 스레드·RT 우선순위를 조정하면 지터도 줄어든다.

THP/HugePages와 IOMMU 설정도 함께 다뤄야 한다. HugePages는 TLB 미스와 페이지 워크를 줄이고, IOMMU/ATS 설정과 PCIe ACS는 DMA 경로를 안정화한다.

컨테이너 스케줄러 측면에서는 k8s device plugin, time-slicing/MIG 인식 스케줄링, cgroups 메모리·CPU·RT runtime 할당으로 멀티테넌시 QoS를 확보한다.

관측과 피드백 루프에서는 Nsight Systems/Compute, CUPTI, TF Profiler로 커널 타임라인·메모리 이벤트·페이지 폴트를 계측해 병목 위치를 정량화한다. tf.data AUTOTUNE, CUDA 런타임 튜닝(블록/그리드/공유메모리), 동적 배치 크기로 폐루프 기반 오토튜닝을 걸어 SLA를 유지한다.

실제 사례

다중 테넌트 실시간 추론에서는 지연시간 SLO를 지키는 것이 목표다. A100/MIG를 1x7g.40gb + 2x3g.20gb로 파티션 설계하고, 지연 민감 서비스는 7g에, 일반 서비스는 3g에 할당한다. 서비스 내 고/저우선 트래픽은 서로 다른 스트림 우선순위로 분리하고, k8s 스케줄러에는 토폴로지 인식 배치(NodeFeatureDiscovery+device-plugin)를 적용한다. 이 구성으로 p99 지연이 3060% 감소하고 노이즈 네이버 영향이 최소화되지만, MIG 세분화 수준에 따라 효율이 515% 손실될 수 있다.

대규모 학습에서는 입력 파이프라인과 메모리 파편화 억제가 관건이다. tf.data map(num_parallel_calls=AUTOTUNE)과 prefetch(device)로 입력 병목을 없애고, TF memory growth와 virtual device memory cap으로 초기 과할당을 막으면서 스텝 진행 중 BFC coalescing을 모니터링한다. XLA를 활성화해 버퍼 재사용을 늘리고 H2D는 핀드 메모리를 쓴다. 그 결과 GPU 유휴 시간이 2040% 감소하고 스텝당 처리량이 1025% 향상되며, 메모리 파편화로 인한 OOM 빈도가 급감한다.

혼합 워크로드(K8s)에서 MPS 기반 공유를 쓸 때는 nvidia-cuda-mps-control 데몬을 기동하고 쿠버네티스 파드별로 CUDA_MPS_ACTIVE_THREAD_PERCENT를 제한한다. cgroups CPU/메모리 상한을 설정하고 RT 스레드 허용치를 보정하며, CUPTI 지표로 SM 점유·컨텍스트 스위치를 모니터링해 임계치를 초과하면 파드를 축출하거나 스로틀링한다. 이 접근은 컨텍스트 스위칭 오버헤드를 1020% 줄이고 처리량을 515% 늘리지만, 간헐적 지연 변동성은 MIG 대비 다소 높다.

UVM 페이지 폴트·Host OOM 장애에 대응할 때는 UVM 폴트 카운터를 모니터링하다가 폴트가 폭주하면 배치 사이즈를 자동으로 축소한다. Host 메모리 cgroup 상한과 OOM score를 조정하고 로그 기반으로 원인을 빠르게 식별하며, HugePages를 적용해 폴트 처리 비용을 절감한다. 그 결과 장애 회피율이 늘고 폴트 처리 시간이 15~30% 줄어든다.

접근 성능 확장성 일관성 안정성 운영 편의
CUDA Streams Only 높음 높음
MPS 중~고 높음
MIG 중~낮음

실제 수치는 GPU 세대(CUDA 11/12, Ampere/Hopper), 커널 특성, 드라이버/TF 버전에 따라 달라지므로 최신 정보를 확인해야 한다.

구현 예시

CUDA: 우선순위 스트림 기반 스케줄링

환경: CUDA 12.x, 컴퓨트 캡처 7.0+(우선순위 효과 권장), Linux x86_64

// nvcc -O2 stream_priority.cu -o stream_priority
#include <cuda_runtime.h>
#include <cstdio>

__global__ void busy_kernel(int iters) {
  float x = 0;
  for (int i = 0; i < iters * 1000; ++i) x = sinf(x) + cosf(x);
  if (threadIdx.x == 0 && blockIdx.x == 0) asm volatile("");
}

int main() {
  int lowPri, highPri;
  cudaDeviceGetStreamPriorityRange(&lowPri, &highPri); // lower value = higher priority
  cudaStream_t s_high, s_low;
  cudaStreamCreateWithPriority(&s_high, cudaStreamNonBlocking, highPri);
  cudaStreamCreateWithPriority(&s_low,  cudaStreamNonBlocking, lowPri);

  dim3 grid(256), block(128);
  // 저우선 스트림에 대규모 커널
  busy_kernel<<<grid, block, 0, s_low>>>(2000);
  // 고우선 스트림에 짧은 커널(지연 민감)
  for (int i = 0; i < 50; ++i) {
    busy_kernel<<<8, 128, 0, s_high>>>(200);
  }

  cudaStreamSynchronize(s_high);
  cudaStreamSynchronize(s_low);
  cudaStreamDestroy(s_high);
  cudaStreamDestroy(s_low);
  return 0;
}

MPS 운영 팁: 서버에서는 nvidia-cuda-mps-control을 시작하고 CUDA_MPS_ACTIVE_THREAD_PERCENT로 파드별 점유를 제한한다. 컨테이너에서는 CUDA_MPS_PIPE_DIRECTORY/CUDA_MPS_LOG_DIRECTORY를 공유 마운트한다.

TensorFlow: 메모리 성장, 상한, XLA, tf.data 튜닝

환경: TensorFlow 2.15+ 또는 2.16+, CUDA 12.x, cuDNN 8.x

import os, tensorflow as tf

# XLA(JIT) 활성화
tf.config.optimizer.set_jit(True)

# GPU 메모리 성장 + 상한 설정
gpus = tf.config.list_physical_devices('GPU')
if gpus:
    try:
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
        # 첫 번째 GPU에 20GB 상한 예시
        tf.config.set_logical_device_configuration(
            gpus[0],
            [tf.config.LogicalDeviceConfiguration(memory_limit=20480)]
        )
    except RuntimeError as e:
        print("RuntimeError:", e)

# tf.data 파이프라인
AUTO = tf.data.AUTOTUNE
ds = (tf.data.TFRecordDataset(["train.tfrecord"])
      .shuffle(8192)
      .map(parse_example, num_parallel_calls=AUTO)
      .batch(256, drop_remainder=True)
      .prefetch(AUTO))

# GPU로 직접 prefetch(전송 버퍼 파이프라인화)
from tensorflow.python.data.experimental.ops import prefetching_ops
ds = prefetching_ops.prefetch_to_device("/GPU:0", buffer_size=2)(ds)

# 간단한 모델 학습
model = build_model()
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy")
model.fit(ds, epochs=10)

NUMA/CPU 바인딩은 Linux에서 numactl --cpunodebind=0 --membind=0 python train.py처럼 적용하고, k8s에서는 cpuManagerPolicy=static과 guaranteed QoS로 CPU 코어를 고정 배정한다.

운영 절차

스케줄 요청디바이스할당(cgroups/device-plugin)컨텍스트 생성/커널 제출정책 적용(MPS/MIG/스트림우선순위)메모리 할당 요청(BFC)시도: alloc/freeUVM 확인아니오데이터 준비 완료실행 상태/프리엠션 신호메트릭수집(Nsight/CUPTI/TFProfiler)할당 실패(OOM)피드백: 배치/우선순위/리소스재조정워크로드오케스트레이터/스케줄러(K8s)OS/런타임 계층CUDA 드라이버GPU SM 스케줄러TensorFlow 런타임OS VMM/NUMA페이지 폴트 여부?UVM 폴트 핸들러/DMA 전송실행 지속모니터링/오토튜너에러 경로: 배치 축소/작업 종료

이 접근들을 종합해서 적용하면 GPU 유휴 시간은 2040% 줄고 처리량은 1030% 늘어나며, p99 지연은 MIG·격리를 적용했을 때 3060% 감소한다. Host 메모리 풋프린트는 XLA+BFC 튜닝으로 2030% 절감되고, MPS 적용 시 컨텍스트 스위칭 오버헤드는 10~20% 줄어든다. 노이즈 네이버가 억제되고 운영 안정성이 높아지며 장애 분석 시간이 단축되는 효과도 있다. 자원 사용 정책을 코드와 플랫폼 전반에서 일관되게 관리할 수 있게 되는 것도 부수적인 이점이다.

GPU 스케줄링과 OS 메모리 관리는 서로 종속적인 관계다. CUDA의 스트림·MPS·MIG로 실행 공정성과 격리를 확보하고, TensorFlow의 BFC/XLA와 tf.data로 메모리·입력 병목을 제거하는 통합 접근이 필요하다. NUMA/HugePages·cgroups 같은 OS 기능과 관측·오토튜닝 루프를 결합해 p99 지연과 처리량을 동시에 최적화하는 체계를 구축하는 것이 권장된다.

GPU스케줄링CUDATensorFlowOS메모리관리MLOps