cpu 사용량 확인하기

2025. 1. 7. 23:07카테고리 없음

1. Pytorch Profiler 사용하기

import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
from torch.profiler import profile, ProfilerActivity

# 모델 정의
class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(10, 1)

    def forward(self, x):
        return self.fc(x)

# DDP 초기화
dist.init_process_group("nccl")
torch.cuda.set_device(0)  # GPU 0 사용
device = torch.device("cuda:0")
model = SimpleModel().to(device)
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[0])

optimizer = optim.SGD(model.parameters(), lr=0.01)

# 데이터
inputs = torch.randn(32, 10).to(device)

# PyTorch Profiler 사용
with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
             on_trace_ready=torch.profiler.tensorboard_trace_handler('./log')) as prof:
    outputs = model(inputs)
    loss = torch.nn.functional.mse_loss(outputs, torch.randn(32, 1).to(device))
    loss.backward()
    optimizer.step()

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=10))

2. psutil로 cpu 추적

import psutil
import os

def monitor_cpu():
    pid = os.getpid()  # 현재 프로세스 ID
    process = psutil.Process(pid)
    print(f"CPU 사용률: {process.cpu_percent(interval=1.0)}%")
    print(f"메모리 사용량: {process.memory_info().rss / 1024 ** 2:.2f} MB")

# 학습 루프 중 호출
for epoch in range(10):
    monitor_cpu()
    outputs = model(inputs)
    loss = torch.nn.functional.mse_loss(outputs, torch.randn(32, 1).to(device))
    loss.backward()
    optimizer.step()

3. DDP 통신 병목 검토

PyTorch Profiler는 DDP 통신 시간(예: all_reduce)을 분석할 수 있음. ProfilerActivity.CUDA와 ddp 이벤트를 활성화 함

import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
from torch.profiler import profile, ProfilerActivity

with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
             on_trace_ready=torch.profiler.tensorboard_trace_handler('./log'),
             record_shapes=True,
             with_stack=True) as prof:
    for epoch in range(10):
        outputs = model(inputs)
        loss = torch.nn.functional.mse_loss(outputs, torch.randn(32, 1).to(device))
        loss.backward()
        optimizer.step()

print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))

NCCL timeout 에러는 Distributed Data Parallel (DDP) 환경에서 GPU 간 통신 또는 데이터 준비 단계에서 병목이 발생할 때 흔히 발생합니다. 특히, 한 GPU에서 사용하는 CPU 리소스를 다른 GPU가 경쟁적으로 사용하면 병목이 발생하고, 이는 NCCL timeout 문제로 이어질 수 있습니다.

이를 해결하기 위해 GPU 간 CPU 리소스 사용을 분리하는 방법을 아래와 같이 정리했습니다.


1. 각 GPU에 별도의 CPU 프로세스를 할당

(1) DDP에서 set_affinity 사용

  • 각 GPU 프로세스에서 특정 CPU 코어만 사용하도록 고정(CPU affinity)하면 충돌을 방지할 수 있습니다.
  • Python의 os.sched_setaffinity를 활용하여 프로세스에 CPU 코어를 명시적으로 할당할 수 있습니다.

코드 예제:

import os
import torch
import torch.distributed as dist

def set_affinity(gpu_rank):
    cpu_count = os.cpu_count()
    cpu_per_gpu = cpu_count // torch.cuda.device_count()
    start_cpu = gpu_rank * cpu_per_gpu
    end_cpu = start_cpu + cpu_per_gpu
    os.sched_setaffinity(0, list(range(start_cpu, end_cpu)))  # 현재 프로세스에 할당된 CPU 설정

# DDP 초기화
dist.init_process_group(backend='nccl')
gpu_rank = dist.get_rank()
torch.cuda.set_device(gpu_rank)

# CPU affinity 설정
set_affinity(gpu_rank)

# 모델 학습 코드
model = ...
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[gpu_rank])

(2) taskset으로 CPU 코어 제어

  • 실행 시 명령어에 taskset을 사용하여 각 GPU 프로세스가 특정 CPU 코어만 사용하도록 설정할 수 있습니다.

실행 예제:

taskset -c 0-3 python train.py --rank 0 &
taskset -c 4-7 python train.py --rank 1 &
taskset -c 8-11 python train.py --rank 2 &
taskset -c 12-15 python train.py --rank 3 &

위에서 taskset은 GPU 0은 CPU 코어 0-3, GPU 1은 CPU 코어 4-7 등을 사용하도록 제한합니다.


2. 데이터 로더 프로세스 분리

  • DDP에서 데이터 준비 단계(데이터 로딩, 전처리 등)가 CPU 자원을 많이 소모합니다. 데이터 로더가 특정 CPU 코어를 과도하게 사용할 경우, 다른 GPU와 충돌을 일으킬 수 있습니다.

(1) num_workersworker_init_fn 조정

  • torch.utils.data.DataLoader에서 num_workers를 GPU별로 조정하고, worker_init_fn으로 CPU 코어를 고정합니다.

코드 예제:

from torch.utils.data import DataLoader

def worker_init_fn(worker_id):
    # 워커 프로세스가 특정 CPU 코어를 사용하도록 고정
    gpu_rank = int(os.environ["LOCAL_RANK"])  # GPU ID 가져오기
    num_cpus = os.cpu_count()
    cpu_per_gpu = num_cpus // torch.cuda.device_count()
    base_cpu = gpu_rank * cpu_per_gpu
    os.sched_setaffinity(0, list(range(base_cpu, base_cpu + cpu_per_gpu)))

dataloader = DataLoader(
    dataset,
    batch_size=32,
    num_workers=4,
    pin_memory=True,
    worker_init_fn=worker_init_fn
)

(2) prefetch_factor 설정

  • DataLoaderprefetch_factor를 조정하여 CPU 워커 프로세스가 한 번에 준비하는 데이터 수를 제한합니다.
  • 문제 상황:
    • 한 GPU 프로세스가 너무 많은 데이터를 미리 로드하여 다른 GPU와 충돌.
  • 해결 방법:
    dataloader = DataLoader(
        dataset,
        batch_size=32,
        num_workers=4,
        prefetch_factor=2,  # 기본값 2
        pin_memory=True
    )

3. NCCL 통신 시간 제한 조정

NCCL timeout 문제는 GPU 간 통신 지연으로 발생할 수도 있습니다. 아래와 같은 방법으로 이를 완화할 수 있습니다.

(1) NCCL 환경 변수 설정

  • NCCL 통신 시간 제한 값을 늘려 GPU 간 통신이 더 오래 걸려도 실패하지 않도록 설정합니다.
  • 실행 전에 환경 변수를 설정:
    export NCCL_DEBUG=INFO
    export NCCL_SOCKET_IFNAME=eth0   # 통신에 사용할 네트워크 인터페이스
    export NCCL_TIMEOUT=180          # NCCL 타임아웃 (기본값 30초)

(2) NCCL 디버그 정보 확인

  • NCCL_DEBUG=INFO로 NCCL 통신 상태를 로그로 출력하여 병목 원인을 파악할 수 있습니다.

4. 데이터셋 분할 및 로드 전략

DDP 환경에서 데이터셋 로드 전략을 GPU별로 적절히 설정해야 합니다.

(1) DistributedSampler 사용

  • 각 GPU가 데이터셋의 서로 다른 부분을 처리하도록 torch.utils.data.distributed.DistributedSampler를 사용합니다.

코드 예제:

from torch.utils.data.distributed import DistributedSampler

sampler = DistributedSampler(dataset, num_replicas=torch.cuda.device_count(), rank=gpu_rank)
dataloader = DataLoader(dataset, batch_size=32, sampler=sampler, num_workers=4, pin_memory=True)

(2) 데이터 전송 최적화

  • pin_memory=True를 사용하여 CPU에서 GPU로 데이터를 전송할 때 더 빠르게 처리합니다.
  • 데이터가 로컬 디스크에서 로드될 경우, 데이터 로컬리티를 확인하여 I/O 병목을 방지.

5. 디버깅과 테스트

  • 문제의 원인을 정확히 파악하려면 작은 데이터셋과 짧은 학습 루프로 디버깅하세요.

  • PyTorch Profiler를 사용하여 각 GPU 및 CPU의 사용 상태를 분석:

    from torch.profiler import profile, ProfilerActivity
    
    with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
                 on_trace_ready=torch.profiler.tensorboard_trace_handler('./log')) as prof:
        outputs = model(inputs)
        loss.backward()
    print(prof.key_averages().table(sort_by="cuda_time_total"))

요약

  1. CPU 리소스 분리:
    • os.sched_setaffinity 또는 taskset으로 GPU별로 고정된 CPU 코어를 사용.
    • 데이터 로더에서 worker_init_fn으로 CPU 자원을 분리.
  2. NCCL 설정 조정:
    • NCCL_TIMEOUT 값 증가.
    • NCCL_DEBUG=INFO로 문제의 원인 파악.
  3. 데이터 로딩 최적화:
    • DistributedSampler로 데이터셋을 GPU별로 나누어 처리.
    • num_workers, prefetch_factor 설정 최적화.
  4. 디버깅:
    • PyTorch Profiler 또는 NCCL 디버그 로그를 활용.