딥러닝 GPU 스케줄링과 운영체제 최적화
TensorFlow와 PyTorch의 GPU 스케줄링, 메모리·I/O·분산 통신을 운영체제 관점에서 정리한다.
2026-08-14 · 최초 발행 2026-01-16
GPU를 쓰는 학습 작업은 운영체제 자원 관리와 맞닿아 있다
TensorFlow와 PyTorch가 GPU에서 연산을 실행하는 과정은 프레임워크 내부에만 머물지 않는다. GPU 자원 배분, 메모리 할당, 데이터 공급, 노드 간 통신은 운영체제와 드라이버 계층의 지원을 전제로 한다.
AI 워크로드에는 대규모 행렬 연산이 많아 GPU CUDA 코어의 병렬 처리가 필요하다. 모델 파라미터와 배치 데이터로 수십 GB의 메모리를 사용하기도 하며, 학습은 순차 읽기, 추론은 랜덤 액세스처럼 I/O 양상도 달라진다. 실행 시간이 수일~수주에 이르므로 안정성 역시 중요하다. 대규모 모델에서는 멀티 GPU·멀티 노드 병렬화도 고려 대상이 된다.
CUDA 요청이 GPU 실행으로 이어지는 경로
CUDA Driver는 사용자 공간 라이브러리로서 GPU 명령을 전달한다. 하드웨어를 직접 제어하는 계층은 리눅스의 nvidia.ko 모듈인 Kernel Driver다. CUDA Context는 GPU 메모리 공간, 스트림, 이벤트를 관리하는 단위이며, CUDA Stream은 비동기 연산을 쌓아 병렬 파이프라인을 구성하는 큐다.
GPU 자원 공유 방식도 작업의 성격에 따라 달라진다.
- Compute Mode는 Default(다중 프로세스 공유), Exclusive(단일 프로세스), Prohibited 모드를 제공한다.
- MPS(Multi-Process Service)는 GPU를 시간 분할로 공유하며 오버헤드를 줄인다.
- MIG(Multi-Instance GPU)는 A100/H100에서 물리적 파티셔닝으로 격리된 인스턴스를 만든다.
- Time-Slicing은 CUDA 10부터 지원하며, 문맥 전환으로 여러 작업을 공존시킨다.
TensorFlow에서는 XLA(Accelerated Linear Algebra)가 JIT 컴파일러로서 연산 융합을 최적화하고, TensorFlow Profiler로 GPU 사용률과 메모리 대역폭을 분석할 수 있다. GPU 메모리 할당에는 allow_growth=True, per_process_gpu_memory_fraction, visible_device_list 설정이 사용된다.
PyTorch는 CUDA Caching Allocator로 메모리를 풀링해 재할당 오버헤드를 낮춘다. torch.cuda.stream()은 커스텀 스트림과 비동기 연산 제어에 쓰이며, DataLoader의 num_workers는 CPU-GPU 데이터 로딩 파이프라인에 관여한다. Mixed Precision(AMP)은 FP16 연산으로 처리량 2배, 메모리 50% 절감을 목표로 한다.
GPU 메모리와 호스트 메모리를 다루는 방식
Unified Memory는 CPU 메모리와 GPU 메모리 사이의 페이지 마이그레이션을 기반으로 동작한다. 접근 패턴이 CPU 집약적인지 GPU 집약적인지에 따라 우선 배치가 달라진다.
GPU 메모리 계층에는 GDDR6/HBM2 기반의 Global Memory가 있으며, 수십 GB 용량과 수백 GB/s 대역폭을 제공한다. Shared Memory는 SM당 수십 KB의 온칩 SRAM으로 저지연 접근에 쓰인다. L1/L2 Cache는 하드웨어가 관리하며 데이터 재사용을 최적화하고, Constant Memory는 읽기 전용 데이터의 브로드캐스트에 적합하다.
운영체제 차원에서는 Huge Pages로 2MB/1GB 페이지를 사용해 TLB 미스를 낮출 수 있다. NUMA 환경에서는 GPU와 가까운 CPU 메모리 노드를 할당하고, cudaMallocHost로 DMA 가능한 pinned memory를 확보한다. 메모리 부족 상황에서는 vm.overcommit_memory, oom_score_adj를 통한 OOM Killer 조정도 검토 대상이다.
메모리가 부족할 때는 중간 활성화 값을 재계산하는 Gradient Checkpointing, 여러 GPU에 모델을 나누는 Model Parallelism, 선택적으로 중간 결과를 보관하는 Activation Checkpointing을 사용할 수 있다. ZeRO-Offload처럼 CPU 메모리나 NVMe SSD로 데이터를 일시 이동하는 Offloading도 한 방법이다.
데이터가 GPU 연산을 기다리지 않게 만드는 I/O 경로
학습 파이프라인에서 스토리지 읽기, 디코딩과 증강, 메모리 버퍼링, GPU 전송 중 어느 한 단계가 느려도 GPU 연산이 멈출 수 있다. 프리페칭과 멀티 워커는 이 경로를 겹쳐 실행하기 위한 수단이다.
대용량 순차 읽기에는 페이지 캐시를 우회하는 Direct I/O를 고려할 수 있다. io_uring은 비동기 I/O로 시스템 콜 오버헤드를 낮추고, SPDK(Storage Performance Development Kit)는 커널을 우회해 스토리지에 접근한다. NVMe-oF는 RDMA를 이용해 네트워크 스토리지를 로컬처럼 사용할 수 있게 한다.
데이터 포맷도 병목에 영향을 준다. TensorFlow의 TFRecord는 직렬화 바이너리 포맷으로 순차 접근에 적합하고, PyTorch의 WebDataset은 tar 기반 스트리밍 데이터셋이다. Parquet/Arrow는 컬럼 기반 포맷으로 압축률이 우수하며, HDF5는 계층 구조와 부분 읽기를 지원한다.
캐싱에서는 .cache()를 통한 데이터셋 메모리·디스크 캐싱, Feast/Hopsworks 기반 Feature Store, Alluxio·Ray Dataset 기반 분산 캐싱을 활용할 수 있다. NVMe를 데이터 캐시로 쓰는 방식은 DRAM 대비 저비용이라는 특성이 있다.
분산 학습에서는 통신 경로가 모델 실행의 일부가 된다
Data Parallelism은 데이터를 나누고 각 GPU가 전체 모델 복사본을 갖는 방식이다. Model Parallelism은 모델을 분할해 각 GPU가 일부 레이어를 맡는다. Pipeline Parallelism은 레이어를 파이프라인 스테이지로 나누며, Hybrid Parallelism은 이 방식들을 결합한다. Megatron-LM이 그 조합의 예다.
NCCL(NVIDIA Collective Communications Library)은 GPU 집합 통신을 담당한다. All-Reduce는 모든 GPU의 그래디언트를 합산한 뒤 브로드캐스트한다. Ring All-Reduce는 대역폭 최적화와 홉 수 최소화에, Tree All-Reduce는 지연 시간 최적화와 계층형 통신에 쓰인다. GPUDirect RDMA는 CPU를 거치지 않고 GPU와 NIC를 직접 연결한다.
통신량 자체를 줄이거나 숨기는 방법도 있다. Gradient Compression은 그래디언트 양자화와 Top-k 스파시티를 이용하며, Overlap Communication은 연산과 통신을 파이프라인으로 겹친다. 노드 내부 NVLink와 노드 간 RDMA를 잇는 Hierarchical All-Reduce, 작은 텐서를 묶어 통신 횟수를 줄이는 Bucketing도 활용할 수 있다.
TensorFlow 실행과 입력 파이프라인 최적화
TensorFlow의 Eager Execution은 즉시 실행되어 디버깅하기 쉽지만 오버헤드가 존재한다. Graph Mode는 정적 그래프 컴파일을 통해 최적화할 수 있고 성능에 유리하다. @tf.function은 파이썬 함수를 그래프로 자동 변환한다.
XLA는 여러 연산을 단일 커널로 병합하는 Operation Fusion, 컴파일 시점 상수 연산인 Constant Folding, 메모리 접근 패턴을 조정하는 Layout Optimization, 임시 버퍼를 다시 쓰는 Buffer Reuse를 수행한다.
tf.data 파이프라인에서는 병렬 읽기와 변환, 캐싱, 셔플, 배치, 프리페칭을 연결할 수 있다.
dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset, num_parallel_calls=AUTOTUNE)
dataset = dataset.map(parse_fn, num_parallel_calls=AUTOTUNE)
dataset = dataset.cache()
dataset = dataset.shuffle(buffer_size=10000)
dataset = dataset.batch(batch_size)
dataset = dataset.prefetch(AUTOTUNE)
Mixed Precision Training에서는 Tensor Core를 활용하는 FP16 연산으로 2배 처리량을 기대할 수 있다. Loss Scaling은 언더플로우를 막기 위해 동적 스케일을 조정하며, tf.keras.mixed_precision.Policy('mixed_float16') 설정을 사용할 수 있다.
PyTorch에서 확인할 실행·분석 지점
PyTorch Distributed 전략 중 DDP(DistributedDataParallel)는 권장 방식으로 효율적인 All-Reduce를 제공한다. DataParallel은 단일 노드 전용의 레거시 방식이다. FSDP(Fully Sharded Data Parallel)는 ZeRO Stage 3 구현으로 메모리 효율을 높이고, RPC(Remote Procedure Call)는 비대칭 워크로드의 모델 병렬화에 사용된다.
TorchScript에서는 torch.jit.trace가 실행 추적 기반으로 그래프를 만들고, torch.jit.script는 파이썬 코드를 직접 파싱한다. CUDA 커널을 합치는 Operator Fusion과 불변 값을 최적화하는 Constant Propagation도 관련된다.
프로파일러는 CPU와 CUDA 활동을 함께 수집해 학습 구간을 분석할 수 있다.
with torch.profiler.profile(
activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA],
schedule=torch.profiler.schedule(wait=1, warmup=1, active=3),
on_trace_ready=torch.profiler.tensorboard_trace_handler('./log')
) as prof:
train()
여기서 GPU Utilization은 SM 점유율을, Memory Bandwidth는 DRAM 접근 병목을, Kernel Time은 각 CUDA 커널의 실행 시간을 확인하는 데 쓰인다.
운영 환경에서 함께 점검할 설정
하드웨어에서는 프레임워크와 호환되는 CUDA Toolkit을 확인하고, 최신 안정 버전의 NVIDIA Driver(예: 535.x), 최신 cuDNN을 관리한다. GPU 클럭은 Persistence Mode를 활성화해 유지할 수 있다.
- GPU 클럭: Persistence Mode 활성화 (
nvidia-smi -pm 1) - 전력 제한: P0 상태 고정, 최대 성능 모드
OS 설정에서는 CPU Governor를 performance 모드로 두고, Transparent Huge Pages를 madvise 또는 always로 설정할 수 있다. NUMA 환경에서는 numactl --cpunodebind로 명시적 바인딩을 적용하며, 네트워크 카드 IRQ를 특정 CPU 코어에 고정하는 IRQ Affinity도 고려한다.
소프트웨어 측면에서는 NVIDIA Container Toolkit을 설치하고, nvidia-smi dmon, dcgm-exporter로 벤치마크와 모니터링을 수행한다. 프로덕션 로그는 WARNING 이상만 출력하며, 체크포인트를 주기적으로 저장해 재시작 지점을 확보한다.