[PyTorch] PyTorch Lightning 그리고 Distributed Computing

2024. 8. 17. 17:00Developers 공간 [Basic]/Software Basic

728x90
반응형

PyTorch Lightning은 PyTorch 코드를 구조화하고 간결하게 만들어주는 라이브러리로, 모델 학습 & 검증을 관리하기 쉽게 해줍니다. 

 

이번 글에서는 PyTorch Lightning을 사용하는 방법과 Multi-Node를 포함한 Distributed Computing 방법들을 살펴보려고합니다.

 

그리고 마지막엔 PyTorch Lightning을 Multi-node distributed computing을 위해 사용하는 과정을 간단히 보이겠습니다.

<구성>
1. PyTorch Lightning
2. Distributed Computing
    a. 다양한 Distributed Computing
    b. Multi Node Setting
    c. Horovod
    d. OpenMPI
    e. Deepspeed
3. PyTorch Lightning for Multi-Node
    a. Implementation
    b. Checklists

글효과 분류1 : 코드

글효과 분류2 : 폴더/파일

글효과 분류3 : 용어설명

글효과 분류4 : 글 내 참조

글효과 분류5 : 글 내 참조2

글효과 분류6 : 글 내 참조3


1. PyTorch Lightning

먼저 PyTorch Lightning을 구현할 때의 과정을 살펴보려기 위해, 학습하는 기본적인 코드 구조와 실행 코드를 보겠습니다.

 

1. Dataloader (≒PyTorch)

 

데이터를 정의할 때는 PyTorch와 같이 정의합니다.

아시다시피 __getitem__함수를 통해 아이템 하나를 가져오는 과정을 정의하는 것이 중요 합니다.

class MyDataset(torch.utils.data.Dataset):
	def __init__(self):
    	raise NotImpelentedError
        
	def __len__(self):
    	raise NotImpelentedError
        
	def __getitem__(self,idx):
    	raise NotImpelentedError

 

이제 위 데이터셋을 활용해 데이터를 분배해 줄 Dataloader를 아래와 같이 PyTorch그대로 구현해줍니다.

아시다시피 역시나 collate_fn을 추가로 정의해 batch화된 데이터를 중간 처리해주는 과정을 추가해줄 수도 있습니다.

train_set = MyDataset()
batch_size = 4
num_workers = 2
def collation_fn(samples):
        batched = list(zip(*samples))
        result = []
        for b in batched:
            if isinstance(b[0], (int, float)):
                b = np.array(b)
            elif isinstance(b[0], torch.Tensor):
                b = torch.stack(b)
            elif isinstance(b[0], np.ndarray):
                b = np.array(b)
            else:
                b = b
            result.append(b)
        return result

dataloader = torch.utils.data.DataLoader(
		train_set, 
        batch_size, 
        shuffle=True,
        num_workers=num_workers, 
        persistent_workers=True, 
        pin_memory=True, 
        drop_last=True, 
        collate_fn=collation_fn)

 


 

2. Model (≒PyTorch)

 

다음으로, 모델PyTorch와 같이 정의합니다. 

아시다시피 forward함수를 통해 forward과정을 정의합니다.

class MyModel(torch.nn.Module):
	def __init__(self):
    	raise NotImpelentedError
        
	def forward(self, input):
    	raise NotImpelentedError
        
model = MyModel()

 


 

3. Training Wrapper

 

자 이제 PyTorch와 같이 모델 선언해주었으니, 이를 활용해 PyTorch Lightning으로 wrapping하는 과정을 살펴보겠습니다.

import pytorch_lightning as pl

 

먼저, LightningModule을 상속해, 우리가 선언했던 모델이 뒤에 나올 PyTorch Lightningtrainer와 compatible하도록 만들어줍니다.

  • 초기화시 모델을 할당해주며, 이를 이용한 training_step함수는 학습과정을 위해 정의해주어야합니다.
  • 또한 추가적으로 configure_optimizers함수 또한 optimizerLR scheduler를 정의하기 위해 정의해주면 좋습니다.
  • 나머지는 해당링크를 참조하시면 좋습니다.
    ** https://lightning.ai/docs/pytorch/stable/common/lightning_module.html
class MyWrapper(pl.LightningModule):
    def __init__(self, model)
        self.model = model

    # Training
    def training_step(self, batch, batch_idx):
        raise NotImpelentedError

    def configure_optimizers(self):
        raise NotImpelentedError

    #def on_train_epoch_end(self):
    #def on_before_zero_grad(self, *args, **kwargs):

    # Validation
    #def validation_step(self, batch, batch_idx):
    #def on_validation_epoch_end(self):

    # Testing
    #def predict_step(self, batch):
    #def forward(self, batch):

training_wrapper = MyWrapper(model)

 

추가적으로, 해당 LightningModule에는 이미 선언된 self.global_rankself.local_rank 값이 내부적으로 있으며 아래와 같은 의미를 가집니다.

  • global_rank : 모든 node와 모든 device에서 현재 process의 index
  • local_rank : 현재 node의 모든 device에서 현재 process의 index

따라서 global rank 0에서만 돌아갈 수 있는 방법은 아래와 같습니다.

  • self.log_dict()self.logLogging에 필요한 함수는 global rank 0에서만 돌아갈수 있는 옵션 rank_zero_only이 있습니다.
  • 위에서 사용한 global_rank를 통해 if self.global_rank == 0:로 global rank 0에서만 돌아가게 할 수 있습니다.

 

4. Callbacks

 

다음으로, 해당 학습이 실행될 때 실행됐으면 하는 Callback함수들을 미리 정의해줍니다. 내부에서 사용할 수 있는 hook은 아래 링크에서 확인 가능합니다.

** https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.Callback.html#lightning.pytorch.callbacks.Callback

from pytorch_lightning.utilities.rank_zero import rank_zero_only

class MyCallback(pl.Callback):
    def __init__(self):
    	raise NotImpelentedError
        
    @rank_zero_only
    @torch.no_grad()
    def on_train_batch_end(self, trainer, module, outputs, batch, batch_idx):
    	raise NotImpelentedError
        
    def on_exception(self, trainer, module, exception):
    	raise NotImpelentedError
        
    def on_save_checkpoint(self, trainer, module, checkpoint):
    	raise NotImpelentedError
        
ckpt_callback = MyCallback()
demo_callback = MyCallback()
exc_callback = MyCallback()
save_model_config_callback = MyCallback()

 

이 때, 상속하는 pl.Callback은 위와 같이 내부 함수에  @rank_zero_only decorator를 @torch.no_grad()처럼 함수 앞에 달아주면 rank 0에서만 실행이 될 것입니다.

 

하지만 pl.callbacks.ModelCheckpoint와 같은 미리 정의된 callback중에는 이미 rank 0에서만 실행이 되는 것도 있습니다. 

** ModelCheckpoint는 설정해서 주지 않아도 자동으로 할당됩니다.

 


 

5. 학습 시작

 

마지막으로, PyTorch Lightning이 본격적으로 학습을 진행할 Trainer를 선언해줍니다. 이때 앞서 선언해준 다양한 callback들을 넣어줍니다.

혹시나 성능을 우선적으로 하고 싶다면 아래와 같은 옵션을 꺼줄 수도 있습니다.

** https://lightning.ai/docs/pytorch/stable/common/trainer.html#trainer-class-api

  • enable_progress_bar = False
  • enable_model_summary = False
  • enable_checkpointing= False 
  • logger = False
  • replace_sampler_ddp = False 
num_gpus =8
num_nodes=1
batch_size=4
strategy = 'ddp_find_unused_parameters_true' if args.num_gpus > 1 else "auto" 

precision="16-mixed"

accum_batches=1
save_dir="./"
gradient_clip_val = 0.0

trainer = pl.Trainer(
        devices=num_gpus,
        num_nodes = num_nodes,
        batch_size = batch_size,
        accelerator="gpu",
        strategy=strategy,
        
        precision=precision,
        callbacks=[ckpt_callback, demo_callback, exc_callback, save_model_config_callback],
        
        accumulate_grad_batches=accum_batches, 
        default_root_dir=save_dir,
        gradient_clip_val=gradient_clip_val,
        
        logger=None,
        log_every_n_steps=1,
        max_epochs=10000000,
        reload_dataloaders_every_n_epochs = 0
    )

 

마지막으로 학습 시작은 아래와 같은 fit함수를 통해 시작합니다. 이 때 앞서 모델을 감싸주었던 training_wrapperdataloader를 넣어줍니다.

ckpt_path='./'

trainer.fit(
    training_wrapper, 
    dataloader, 
    ckpt_path=ckpt_path
)
더보기

--------------------------------------------------------

<Distributed 학습시 주의사항>

 

PyTorch Lightning을 활용해 multi-node 혹은 multi-gpu 학습을 실행할 때 주의할 부분은, 복제된 python파일 전체가 재실행된다는 것입니다.

 

예를 들어 아래와 같은 코드가 있을 때,

def main():
	# ...
	trainer.fit(
		training_wrapper, 
		dataloader, 
		ckpt_path=ckpt_path
	)
	# ...

if __name__=='__main__':
	print("A")
	main()


print함수를 통해 A는 두번 프린트됩니다.

따라서 환경변수 등을 정의할 때 주의하셔야합니다.

--------------------------------------------------------


2. Distributed Computing

 

각각 GPU를 가지고 있는 여러개의 Multi-nodeCluster로 만들어 학습하는 방법까지를 Distributed Computing이라고 합니다.

 

Multi-Node를 위에서 설명한 PyTorch Lightning을 활용해 구현하지 않고, PyTorch로 구현된 코드로 구현하기 위해서는 다양한 방법이 있습니다.

이들에 대해 간단히 보이고, 실행시 주의점을 살펴보겠습니다.


a. 다양한 Distributed Computing

 

PyTorch로 모델을 Multi-Node에서 Distributed 학습하기 위해 사용할 수 있는 주요 패키지 혹은 프레임워크는 다음과 같습니다:

1. PyTorch Distributed (torch.distributed)

 

PyTorch 자체에 내장된 기본적인 분산 학습 프레임워크입니다.

Multi-GPU와 Multi-Node 학습을 모두 지원하며, RPC, DDP(Distributed Data Parallel)와 같은 기능을 제공합니다.

더보기

--------------------------------------------------------

<프로세스간 Communication Backend>

 

분산학습을 위한 backend는 아래와 같은 것들이 있습니다.

  • MPI(Message Passing Interface) : distributed 환경에서 주로 사용하며, mpirun 등의 프로세스를 통해 관리하므로 main함수 하나만 실행해주면 됩니다.
  • NCCL : Distributed GPU training 시 필요하며, CUDA Tensor들에 대한 집합 연산의 최적화된 구현체를 제공합니다. 
  • GLOO : Distributed CPU training 시 필요하며,  CUDA는 NCCL만큼 최적화되어있지 않습니다.
  • RPC(Remote Procedure Calls) : 서버/클라이언트 모델시 필요하며, MPI에 비해 고수준 API입니다.

이중 가장 많이 사용하는 NCCL은 GPU간의 communication을 가능하게 하는 stand-alone 라이브러리로 all-reduce, all-gather, reduce, broadcast, reduce-scatter과 같은 특징이 구현되어 있습니다.

 

내부적으로는 PCIe, NVLink, NVswitch, 그리고 네트워킹(InfiniBand, TCP/IP sockets)을 활용해 높은 bandwidth를 가능하게 최적화되어있습니다.

NCCL는 기존의 PyTorch, Tensorflow와 같은 딥러닝 프레임워크가 horovod등과 활용했을 때 보통 사용되지만, 자체적으로는 C library이기 떄문에 Python에서 C functions를 통해 불러져 python 코드로 단순히 사용할 수 있는 수준의 라이브러리는 아닙니다. 

--------------------------------------------------------

 

기존 DDP와 같이 아래와 같은 셋팅을 해주어야합니다.

  • 모델 래핑 : torch.nn.parallel.DistributedDataParallel를 활용해 모델을 감싸주기
  • 데이터 샘플링 : torch.utils.data.distributed.DistributedSampler를 활용한 Data loader의 sampler선언
  • 프로세스 초기화 : 각 프로세스에서 torch.distributed.init_process_group 를 활용한 초기화
    ** 주의해야할 것은 실행할 학습함수가 local_rank라는 arugment를 받아 사용할수 있도록 해주어야 여기서 활용합니다.
  • (아래 script가 아닌 코드 내의 spawn을 활용하는 경우) torch.multiprocessing.spawn을 활용한 학습함수 복제

그 다음, multi-node로 실행하기 위해서는 아래와 같은 torch.distributed.launch를 사용해 분산 학습을 설정할 수 있습니다.

python3 -m torch.distributed.launch \
	--nproc_per_node=4 \
    --nnodes=2 \
    --node_rank=0 \
    --master_addr="127.0.0.1" \
    --master_port=6666 main.py

 

위 스크립트 이외에도 아래와 같은 작업 스케줄러나 오케스트레이션 도구(예: Slurm, Kubernetes 등)를 사용하여 각 노드에서 자동으로 스크립트가 실행되도록 설정할 수 있습니다.

  • torchrun : PyTorch 1.9버전부터는 torch.distributed.launch 대신 torchrun을 사용하도록 권장한다고 합니다. 더욱 사용하기 쉽게 더욱 다양한 기능을 제공한다고 합니다.
  • SLURM(srun) : HPC(High Performance Computing) 클러스터에서 작업을 관리하는 스케줄러로, 각 node에서 script를 각각 실행하도록 해줍니다. 
  • TorchElastic : PyTorch의 분산 훈련 프레임워크로, 동적 리소스 할당과 Elastic Training을 통해 작업이 중단되거나 실패했을 때 자동으로 복구할 수 있는 기능을 제공하여, 노드 실패 시에도 학습이 지속될 수 있도록 합니다.

작업 스케줄러를 활용한 방법은 이번 글에서는 살펴보지 않을 예정입니다.

 


 

2. Horovod

 

Uber에서 개발한 오픈소스 분산 훈련 프레임워크로, 여러 딥러닝 프레임워크(TensorFlow, PyTorch 등)에서 사용할 수 있습니다. 여러 프레임워크를 지원하고, 대규모 분산 학습 시 확장성과 효율성이 뛰어납니다.

 

PyTorch에서 모델 래핑, 데이터 샘플링, 프로세스 초기화를 이미 진행했던 경우, 그대로 horovodrun을 활용해 분산 학습 프로세스를 시작가능하고 각 노드와 GPU를 자동으로 설정해줍니다.

하지만, 이렇게 실행하면 단순히 프로세스 관리만 도와주는 것이므로, 코드 내부에서 아래와 같은 Horovod API를 활용하해 최적화해 실행할 수 있습니다.

  • 모델 래핑 : MyModel().to(horovod.torch.local_rank())을 통해 모델을 해당 local rank로 보내주어야합니다.
    • 추가적으로 horovod.torch.DistributedOptimizer을 사용해 기존 옵티마이저를 래핑해 효율적인 통신(예: AllReduce)을 수행할 수 있습니다.
  • 데이터 샘플링 : PyTorch와 동일
  • 프로세스 초기화 : 각 프로세스에서 horovod.torch.init() 를 활용한 초기화

하지만 위와 같은 모델 래핑, 데이터 샘플링, 프로세스 초기화 들이 되어있지 않으면 아예 실행이 불가능합니다.

 



3. MPI (Message Passing Interface)

 

다중 노드에서 노드 간 통신을 효율적으로 처리하는 표준 통신 프로토콜로, PyTorch에서 MPI를 기반으로 분산 학습을 설정할 수 있습니다.

 

PyTorch에서 했던 모델 래핑, 데이터 샘플링, 프로세스 초기화를 진행했던 경우 그대로 mpirun을 활용해 실행이 가능하지만, 되어있지 않으면 아예 실행이 불가능합니다.

 


 

4. DeepSpeed

** PyTorch 모델 래핑(DDP), 데이터 샘플링(DistributedSampler), 프로세스 초기화 등 필요 없음

 

단, 기존에 PyTorch로 구현된 코드가 있다면 아래 작업과 같이 deepspeed.initialize()를 통해 래핑은 해주어야합니다.

import deepspeed

model = MyModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

model_engine, optimizer, dataloader, _ = deepspeed.initialize(
    model=model,
    optimizer=optimizer,
    config="ds_config.json",  # 설정 파일만 제공하면 충분
    training_data=my_dataset
)

for step, batch in enumerate(dataloader):
    loss = model_engine(batch)
    model_engine.backward(loss)
    model_engine.step()

 

또한 아래와 같이 DeepSpeed가 제공하는 고성능의 optimizer를 선택적으로 사용할 수도 있습니다.

from deepspeed.ops.adam import DeepSpeedCPUAdam

optimizer = DeepSpeedCPUAdam(model.parameters(), lr=1e-3)

 

Deepspeed는 Microsoft에서 개발한 분산 훈련 및 모델 최적화 라이브러리로 ZeRO(Zero Redundancy Optimizer), Mixed Precision Training, Tensor Slicing, Model Parallelism 등 대규모 모델을 효율적으로 학습하기 위한 다양한 기술을 지원하기 때문에 대규모 모델 학습에 특화되어 있습니다.


 

5. Hugging Face Accelerate

** PyTorch 모델 래핑(DDP), 데이터 샘플링(DistributedSampler), 프로세스 초기화 등 필요 없음

** https://huggingface.co/docs/transformers/v4.29.0/ko/accelerate

 

transformers 라이브러리에서 주로 사용하는 Hugging Face Accelerate는, PyTorch 분산 학습을 매우 간단히 만들어 줍니다.

 

학습 코드는 거의 수정하지 않아도 되며, 아래와 같은 accelerate CLI 도구로 분산 설정을 자동화합니다.

accelerate launch \
    --multi_gpu \
    --num_machines=2 \
    --machine_rank=0 \
    train.py

 


 

6. 기타 다른 프레임워크 및 라이브러리

  • Ray Train
    • 분산 컴퓨팅 프레임워크로, PyTorchTensorFlow와 통합되어 분산 학습을 지원합니다. 
    • 간단한 API로 분산 훈련을 지원하며, 자동으로 자원을 할당하고 관리합니다.
    • Hyperparameter tuning과 같은 추가 기능도 지원합니다.
  • Bagua
    • 분산 데이터 병렬 학습을 위한 최적화된 통신 라이브러리이며, PyTorch와 자연스럽게 통합됩니다.
    • AllReduce, Decentralized Parallel SGD, Low Precision Gradient Compression 등 다양한 통신 최적화 기술을 제공합니다.

b. Multi Node Setting

 

Multi-Node의 경우, 코드셋팅이나 작업 스케줄러를 위한 CLI에 앞서, 미리 환경 셋팅해야 할 부분이 있습니다. 이런 부분들을 살펴보겠습니다.

 

먼저, 환경에 대한 가정을 해보겠습니다.

아래와 같은 3개의 Node를 연결할 생각이며, 각각 아래와 같은 Global IP를 가지고 있다고 생각하겠습니다. 또한 각각의 Node는 GPU 8개를 가지고 있다고 가정하겠습니다.

  • Master(마스터 노드) : 1.2.3.1 
  • Slave1(워커 노드) : 1.2.3.2 
  • Slave2(워커 노드) : 1.2.3.3 

그리고 Mult-Node 학습이 아니었을 때, Single 노드에서 학습시 아래와 같은 명령어를 실행해야되는 상황이라고 가정하겠습니다.

 

python3 train.py \
    --dataset-config data.json \
    --model-config model.json \
    --num-gpus 8 \
    --batch-size 4 \
    --name my_train

 


 

1. 방화벽 열어주기 (Optional)

 

방화벽이 닫혀 있다면 아래 두가지의 포트만은 열어주어야 합니다.

아래 지정된 3333,6666 포트는 필자가 임의로 지정한 포트입니다.

  • ssh를 위한 포트 : 3333
  • data 통신을 위한 포트 : 6666

그럼 두가지 포트에 대한 방화벽을 열어보겠습니다.

sudo ufw status numbered
sudo ufw allow from any to any port 3333 proto tcp
sudo ufw allow from any to any port 6666 proto tcp

 


 

2. Docker Container 생성 (Optional)

 

Docker Container 내부에서 실행하고 싶다면, docker run 할 때 아래와 같은 옵션을 활용하시는 것을 추천합니다.

  • --network host : 호스트의 네트워크 환경을 그대로 사용합니다.
    • bridge(default) : 따로 bridge를 설정해주지 않으면 docker0라는 브리지에 연결될 것입니다.
      ** docker0 : 도커가 설치될 때, 기본적으로 구성되는 브리지입니다. 호스트의 네트워크인 eth0과 컨테이너의 네트워크와 연결을 해주는 역할입니다.
    • host: 호스트의 네트워크 환경을 그대로 사용, 성능 향상 및 최적화가 필요하거나 광범위한 포트 처리를 해야 하는 경우 사용.
    • none : 아무런 네트워크를 사용하지 않는 것
    • container : 해당 컨테이너의 네트워크를 공유
  • --ipc host : 컨테이너가 호스트의 shared memory에 접근이 가능하게 하도록, 호스트의 IPC namespace를 그대로 사용합니다.
    • none : 컨테이너의 private IPC namespace를 가집니다. (/dev/shm mount자체도 안됨)
    • private(default): 컨테이너의의 private IPC namespace를 가집니다.
    • shareable : 컨테이너의의 private IPC namespace를 가집니다. (다른 컨테이너와 공유가 가능합니다.)
    • container:<name-or-ID> : 특정 컨테이너의 IPC namespace를 활용합니다.
    • host : 호스트의 IPC namespace를 그대로 사용
  • --shm_size 256gb : shared memory를 충분히 넉넉하게 잡아주어야 합니다.
    ** shared memory는 일반적으로 IPC(Inter Process Communication)을 위해 활용됩니다. GPU와는 보통 무관합니다.
  • —privileged : 커널접근을 가능하게 하기 때문에 파일시스템 등을 mount하는 등의 작업이 가능하게 합니다.
    ** docker 1.2 버전 이후로, --privileged를 통해 모든 Linux capability를 주는 것이 아닌, --cap-add=SYS_ADMIN과 같이 fine-grained한 capability를 주는 방법이 추가되었습니다.
  • --ulimit memlock=-1  : 메모리가 locked되는 것에 대한 양의 제한을 풀어주는 것입니다. 

예를 들어 Docker instance를 만드는 예시는 아래와 같습니다.

docker run -it \
    --net=host \
    --ipc=host \
    --shm-size=256g \
    --privileged \
    --gpus=all \
    --name=test \
    -v=/storage:/storage \
    nvidia/cuda:10.2-cudnn8-devel-ubuntu18.04 bash

 


3. SSH 포트 열어주기 (선택)

 

컨테이너 밖 또는 안에서 ssh의 daemon이 지켜볼 포트를 우리가 정한 값으로 변경해주어야합니다.

앞서 필자가 ssh포트를 3333으로 지정했기 때문에 변경하는 것이니 기존대로 22를 사용하고 싶다면 패스하셔도 됩니다.

 

먼저 관련된 ssh 패키지를 설치해줍니다.

apt-get install openssh-server
mkdir /var/run/sshd
passwd

 

다음으로 /etc/ssh/sshd_config 파일을 아래와 같이 수정한 후, Daemon을 다시 실행해줍니다.

보통은 컨테이너들은 root user를 활용하므로 아래를 통해 각각의 node의 컨테이너 간에 root로 ssh접근이 가능하도록 해주기 위해 PermitRootLogin도 바꿔줍니다.

...
Port 3333
...
PermitRootLogin yes
...

 

그럼 ssh daemon을 재시작하겠습니다.

# Restart service
sudo service sshd restart # CentOS/Fedora
sudo service ssh restart # Ubuntu/Debian
더보기

 -----------------------------------------------------

<ssh 포트 변경하는 임시적 방법>

 

위는 영구적으로 바꾸는 방법인데, 해당 컨테이너가 살아있는 동안만 하고 싶다면  /etc/ssh/sshd_config 파일을 아래와 같이 수정해준 후에

...
PermitRootLogin yes
...

 

아래와 같이 실행합니다.

/usr/sbin/sshd -p 3333

 -----------------------------------------------------

 

이제 각각의 node간 접근은 서로 가능하지만, master에서 slave에 접근할 때 비밀번호 입력하는 것마저도 생략할 수 있도록 master의 public key를 각각의 서버에 미리 넣어줄 것입니다. 이는 필수적으로 필요합니다.

 

아래는 master에서만 실행합니다.

# Make Public Key
ssh-keygen -t rsa 

# To Master self
cat ~/.ssh/id_rsa.pub | ssh root@1.2.3.1 -p 3333 'cat >> .ssh/authorized_keys'

# To Slave1
ssh root@1.2.3.2 -p 3333 mkdir -p .ssh
cat ~/.ssh/id_rsa.pub | ssh root@1.2.3.2 -p 3333 'cat >> .ssh/authorized_keys'

# To Slave2
ssh root@1.2.3.3 -p 3333 mkdir -p .ssh
cat ~/.ssh/id_rsa.pub | ssh root@1.2.3.3 -p 3333 'cat >> .ssh/authorized_keys'

# Test All
ssh -o PasswordAuthentication=no -p 3333 1.2.3.1
ssh -o PasswordAuthentication=no -p 3333 1.2.3.2
ssh -o PasswordAuthentication=no -p 3333 1.2.3.3
더보기

------------------------------------------------------------

<호스트 네트워크의 정보 기록해두기 (옵션)>

 

혹시 각각의 도메인을 IP로 명시하기 귀찮으면, 호스트 네트워크들을 미리 다른 이름으로 저장해두는 방법도있습니다.


방법1. ~/.ssh/config 혹은 /etc/ssh/config

아래와 같은 형태로 기록해줍니다.

Host slave1
    HostName 1.2.3.2
    Port 3333
Host slave2
    HostName 1.2.3.3
    Port 3333

 

방법2. /etc/hosts
/etc/hosts 파일은 컴퓨터의 로컬 DNS 역할을 하는 파일로, 여기에 각 노드의 도메인 이름과 IP 주소를 등록합니다.

1.2.3.2 slave1
1.2.3.3 slave2

 

위와 같이 변경하면 아래와 같은 명령어로 ssh에 접근이 가능할 것입니다.

# Method1
ssh -o PasswordAuthentication=no slave1 hostname


# Method2
apt-get install -y iputils-ping
ping slave1

 

------------------------------------------------------------


 

4. 학습환경 설정해주기

 

학습을 할 때는 라이브러리 설치 등 환경셋팅이 필수적인데, multi-node는 학습전에 아래와 같은 조건들은 각각의 node에 동일하게 셋팅되어야합니다.

  1. 라이브러리 등 : 같은 버전의 라이브러리가 설치되어있는 것이 좋습니다. 특히나 PyTorch는 버전이 달라 문제가 생기면 알기가 어렵습니다.
  2. 학습실행할 파일실행에 필요한 config파일들 : 모든 노드에서 같은 위치에 위치해야합니다.
  3. 학습에 활용할 데이터 : 모든 노드에서 같은 위치에 위치해야합니다.

 

그럼 이제 이와 같은 셋팅이 완성되었다는 전제 하에 위에서 살펴보았던 Distributed Computing을 위한 프레임워크와 라이브러리를 구현하는 방법을 간단히 살펴보겠습니다.


c. Horovod

 

코드와 관련된 모델 래핑, 데이터 샘플링, 프로세스 초기화 등은 앞서 설명했으므로 생략하고, CLI를 활용해 실행하는 과정만 설명하겠습니다.

 

HorovodGLOO 혹은 MPI가 필요합니다. 여기서는 OpenMPI를 설치하겠습니다.

wget https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-4.1.0.tar.gz
tar -xf openmpi-4.1.0.tar.gz
cd openmpi-4.1.0
./configure --prefix=/usr/local
make all && sudo make install

 

이제 Horovod를 설치합니다.

** 코드 아래와 에러메시지와 같은 에러가 나면 두번째 명령어와 같이 실행해야합니다.

# Install 
HOROVOD_GPU_OPERATIONS=NCCL HOROVOD_WITH_PYTORCH=1 HOROVOD_WITHOUT_GLOO=1 HOROVOD_WITH_MPI=1 pip3 install --no-cache-dir horovod

# Install if c++ version error
HOROVOD_GPU_OPERATIONS=NCCL HOROVOD_WITH_PYTORCH=1 HOROVOD_WITHOUT_GLOO=1 HOROVOD_WITH_MPI=1 pip3 install --no-cache-dir git+https://github.com/thomas-bouvier/horovod.git@compile-cpp17==0.27.0

horovodrun --check-build
error You need C++17 to compile PyTorch
더보기

------------------------------------------------------------

<g++의 버전 확인>

 

위와 같은 문제가 발생한 것은 g++ 버전이 c++17을 지원하지 않거나 g++이 설치가 제대로 되어있지 않기 때문일 수도 있습니다.

gcc --version
apt-get install gcc

g++ --version
apt-get install g++

 

즉, c++17을 하려면 g++의 특정버전이상이 설치되어있어야합니다. 아래표를 참조합니다.

** Ubuntu22.04에는 GCC가 11.x, C++17이 default이고, 
** Ubuntu20.04에는 GCC가 9.x, C++14이 default입니다.

C++ 버전 Modern GCC default GCC 버전 Compiler 옵션
C++26 O   GCC 14 ~ -std=c++2c, 
-std=gnu++2c
C++23 O   GCC 11 ~ -std=c++23, 
-std=c++2b, 
-std=gnu++2b
C++20 O   GCC 8 ~ -std=c++20(GCC 11↑), 
-std=c++2a(GCC 9↓), 
-std=gnu++20
C++17 O GCC 11 GCC 5 ~  -std=c++17,
-std=gnu++17
C++14 O GCC 6.1 GCC 4.9 ~ GCC 10 -std=c++14, 
-std=gnu++14
C++11 O GCC 5.x GCC 4.7 ~ GCC 5.x  -std=c++11, 
-std=gnu++11
C++0x     GCC 4.3 ~ GCC 4.6  
C++98   GCC 6.0  ~ GCC 6.0 -std=c++98

------------------------------------------------------------

 

설치가 되면 아직 노드를 연결하지 않고, 싱글 노드에서 먼저 자체적으로 잘 동작하는지 확인해줍니다.

** NCCL_DEBUG=INFO,  NCCL_DEBUG=WARN : NCCL의 내부적으로 문제가 나는 에러메시지를 프린트하고 싶을 때 사용합니다.

export NCCL_DEBUG=INFO 
horovodrun \
    -np 8 \
    -H 1.2.3.1:8 \
    python3 train.py \
        --dataset-config data.json \
        --model-config model.json \
        --num-gpus 8 \
        --batch-size 4 \
        --name my_train

 

다음으로 아래와 같이 멀티 노드에서 실행해줍니다.

export NCCL_DEBUG=INFO 
horovodrun \
    -np 24 \
    -H 1.2.3.1:8,1.2.3.2:8,1.2.3.3:8 \
    --network-interface eth0 \
    python3 train.py \
        --dataset-config data.json \
        --model-config model.json \
        --num-gpus 8 \
        --batch-size 4 \
		--name my_train

 

SSH포트는 기본적으로 22를 사용하니, 변경하고 싶으면 ~/.ssh/config에 host name에 따른 포트를 조정하는 것을 추천합니다.

 

혹시 PyTorch Lightning에서 멀티 노드 학습을 horovod 전략으로 실행하려면 아래와 같이 strategy를 추가해야합니다.

** deprecated 된 것인지는 모르겠지만, 설명은 있고 동작은 안합니다. 아직까지 정상동작하는 것을 확인하지 못했습니다.

trainer = pl.Trainer(accelerator='horovod')

d. OpenMPI

 

역시나 코드와 관련된 모델 래핑, 데이터 샘플링, 프로세스 초기화 등은 앞서 설명했으므로 생략하고, CLI를 활용해 실행하는 과정만 설명하겠습니다.

 

먼저 OpenMPI를 설치해줍니다.

wget https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-4.1.0.tar.gz
tar -xf openmpi-4.1.0.tar.gz
cd openmpi-4.1.0
./configure --prefix=/usr/local
make all && sudo make install
ldconfig

 

아래는 하나의 노드에서 8개 GPU로 실행할 때의 명령어입니다. 

** 환경변수 OMPI_ALLOW_RUN_AS_ROOT, OMPI_ALLOW_RUN_AS_ROOT_CONFIRMmpirun 실행시 --allow-run-as-root의 경우, 컨테이너에서 root user로 실행하고 싶을 때 사용합니다.

** docker 내부가 아니라면 --mca btl_tcp_if_exclude는 제거해주셔도 좋습니다.

export OMPI_ALLOW_RUN_AS_ROOT=1 
export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
mpirun -np 8 \
    --allow-run-as-root \
    --bind-to none \
    --map-by slot \
    -mca btl_tcp_if_exclude lo,docker0 \
    -mca pml ob1 \
    -mca btl ^openib \
    -x NCCL_DEBUG=INFO \
    -x LD_LIBRARY_PATH \
    -x PATH \
        python3 train.py \
        --dataset-config data.json \
        --model-config model.json \
        --num-gpus 8 \
        --batch-size 4 \
        --name my_train

 

다음으로 아래와 같이 멀티 노드에서 실행해줍니다.

export OMPI_ALLOW_RUN_AS_ROOT=1 
export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
mpirun -np 24 \
    -H 1.2.3.1:8,1.2.3.2:8,1.2.3.3:8 \
    --allow-run-as-root \
    --bind-to none \
    --map-by slot \
    -mca btl_tcp_if_exclude lo,docker0 \
    -mca pml ob1 \
    -mca btl ^openib \
    -x NCCL_DEBUG=INFO \
    -x LD_LIBRARY_PATH \
    -x PATH \
    python3 train.py \
        --dataset-config data.json \
        --model-config model.json \
        --num-gpus 8 \
        --batch-size 4 \
        --name my_train

 

SSH포트는 기본적으로 22를 사용하니, 변경하고 싶으면 ~/.ssh/config에 host name에 따른 포트를 조정하는 것을 추천합니다.


e. Deepspeed

 

Deepspeed에서 필요한 초기화는 위에서 설명했으므로, CLI를 활용해 실행하는 과정만 설명하겠습니다.

 

1. hostfile 만들기

 

아래와 같은 hostfile을 하나씩 만들어 각 노드들에 대해 명시해주어야합니다. slots는 몇개의 GPU를 활용할지 입니다.

1.2.3.1 slots=8
1.2.3.2 slots=8
1.2.3.3 slots=8

 

2. 설치 및 테스트

 

아래 명령어를 통해 deepspeed를 설치해줍니다.

pip3 install deepspeed

 

노드 연결하지 않고, 싱글노드에서 잘 동작하는지 확인해줍니다.

deepspeed \
    -H ./hostfile.txt \
    --include 1.2.3.1 \
    --master_addr 1.2.3.1 \
    --master_port 6666 \
    --ssh_port 3333 \
    train.py \
        --dataset-config data.json \
        --model-config model.json \
        --num-gpus 8 \
        --batch-size 4 \
        --name my_train

 

여러개의 노드에서 코드 없이 잘 연결되어있는지 확인해줍니다.

deepspeed \
    -H ./hostfile \
    --include 1.2.3.1@1.2.3.2@1.2.3.3 \
    --master_addr 1.2.3.1 \
    --master_port 6666 \
    --ssh_port 3333 \
    --no_python --no_local_rank pwd

 

3. 멀티노드 시작시작

 

이번엔 여러개의 노드로 실행해봅니다.

** export NCCL_SOCKET_IFNAME=eth0 해줘야 NIC를 지정할 수 있습니다.

** NIC(Network Interface Card) : 컴퓨터들끼리 전송/수신 등의 네트워킹이 가능하도록 해주는 하드웨어 명으로, ifconfig를 해보면 보통 bond0, bond1, eth0 중에 하나 입니다.

** 만약 node 별로 NIC가 다르다면 export NCCL_SOCKET_IFNAME=eth0,bond0 이렇게 적어주면 됩니다.

export NCCL_SOCKET_IFNAME=eth0
export NCCL_DEBUG=INFO
deepspeed \
    -H ./hostfile.txt \
    --include 1.2.3.1@1.2.3.2@1.2.3.3 \
    --master_addr 1.2.3.1 \
    --master_port 6666 \
    --ssh_port 3333 \
    train.py \
        --dataset-config data.json \
        --model-config model.json \
        --num-gpus 8 \
        --batch-size 4 \
        --name my_train
더보기

-------------------------------------------------------

<InfiniBand(인피니밴드)>

 

위에서 NCCL_IB_DISABLE=1이라는 옵션으로 InfiniBand 기능을 끄는 환경변수도 있습니다.

 

InfiniBand(인피니밴드)는 고성능 컴퓨팅(HPC) 환경 및 분산 시스템에서 주로 사용되며, 고속 데이터 전송과 낮은 지연 시간을 제공하는 네트워크 인터커넥트 기술입니다.

 

주로 데이터 센터, 슈퍼컴퓨터, 클라우드 컴퓨팅 인프라에서 사용되며, 아래와 같은 특징을 활용해 CPU, 메모리, 스토리지, GPU 간의 대용량 데이터를 빠르고 효율적으로 전송하는 데 적합합니다.

  • 높은 대역폭 및 낮은 지연 시간: 기존 이더넷보다 훨씬 높은 대역폭과 더 낮은 지연 시간을 제공합니다. 이는 특히 분산 컴퓨팅 환경에서 많은 데이터를 빠르게 교환해야 할 때 큰 이점이 됩니다.
  • RDMA(Remote Direct Memory Access) 지원 : CPU를 거치지 않고 원격 메모리에 직접 접근해 데이터를 읽거나 쓸 수 있는 기술입니다. 이를 통해 데이터 전송 과정에서 CPU 개입을 최소화하고, 시스템 자원의 효율성을 높여 지연 시간을 크게 줄이는 데 기여합니다.
  • 고확장성 : 수천 대의 노드를 클러스터로 구성할 수 있을 만큼 매우 확장성이 뛰어납니다. 슈퍼컴퓨터 및 대규모 데이터 센터에서 이 기술을 통해 여러 노드를 빠르게 연결하고 동기화할 수 있습니다.
  • 일관된 성능 : 네트워크 트래픽이 많아지더라도 안정적이고 일관된 성능을 유지할 수 있도록 설계되었습니다. 대규모 분산 학습, HPC 작업, 데이터 집약적인 애플리케이션 등에 이상적입니다. 

하지만 이 환경 변수를 DISABLE하는 것은 학습에 영향을 주는 경우가 있어 비활성화에 주의를 가지는 것이 좋습니다.

 

이외에도 NCCL_P2P_DISABLE=1, NCCL_SHM_DISABLE=1와 같은 옵션도 있습니다.

--------------------------------------------------------

 

혹시 PyTorch Lightning에서 멀티 노드 학습을 DeepSpeed 전략으로 실행하려면 아래와 같이 strategy를 추가해야합니다.

더보기

---------------------------------------------------------

<Strategy Variation>

 

1. strategy 객체 : Pytorch Lightning에 정의된 객체로 넣어줍니다.

  • from lightning.pytorch.strategies.strategy import Strategy
  • from lightning.pytorch.strategies.ddp import DDPStrategy
  • from lightning.pytorch.strategies.deepspeed import DeepSpeedStrategy
  • from lightning.pytorch.strategies.fsdp import FSDPStrategy
  • from lightning.pytorch.strategies.model_parallel import ModelParallelStrategy
  • from lightning.pytorch.strategies.parallel import ParallelStrategy
  • from lightning.pytorch.strategies.single_device import SingleDeviceStrategy
  • from lightning.pytorch.strategies.single_xla import SingleDeviceXLAStrategy
  • from lightning.pytorch.strategies.xla import XLAStrategy

 

2. strategy String : 문자열로 넣어줍니다.

- Deep Speed관련

  • "deepspeed" : Deepspeed Strategy의 default
  • "deepspeed_stage_1" : Deepspeed와 ZeRO Stage 1
  • "deepspeed_stage_1_offload" : Deepspeed와 ZeRO stage 1 + optimizer CPU offload
  • "deepspeed_stage_2", "deepspeed_stage_2_offload" : Deepspeed와 ZeRO stage 2 (+ optimizer CPU offload)
  • "deepspeed_stage_3", "deepspeed_stage_3_offload" : Deepspeed와 ZeRO stage 3 (+ optimizer CPU offload)
  • "deepspeed_stage_3_offload_nvme" : Deepspeed와 ZeRO stage 3 + NVMe offload

 

-이외 :

  • "dp", "ddp", ""ddp_spawn", "ddp_fork", "ddp_notebook", "ddp_find_unused_parameters_true", "fsdp", "fsdp_cpu_offload", "single_xla", "xla", "xla_fsdp", "auto"

---------------------------------------------------------

a. strategy 객체로 제공하는 방법

from pl.strategies import DeepSpeedStrategy

trainer = pl.Trainer( 
    accelerator='gpu', 
    strategy=pl.strategies.DeepSpeedStrategy(stage=2, 
                                        contiguous_gradients=True, 
                                        overlap_comm=True, 
                                        reduce_scatter=True, 
                                        reduce_bucket_size=5e8, 
                                        allgather_bucket_size=5e8,
                                        load_full_weights=True),
)

b. string으로 제공하는 방법

trainer = pl.Trainer( 
    accelerator='gpu', 
    strategy="deepspeed_stage_2"
)

c. config 파일로 제공하는 방법 (아래는 deepspeed_config.json이라는 파일)

{
    "train_batch_size": 64,
    "gradient_accumulation_steps": 1,
    "fp16": {
        "enabled": true,
        “loss_scale": 0,
        "loss_scale_window": 1000,
        "hysteresis": 2,
        "min_loss_scale": 1
    },
    "zero_optimization": {
        "stage": 2,
        "allgather_partitions": true,
        "allgather_bucket_size": 5e8,
        "reduce_scatter": true,
        "reduce_bucket_size": 5e8,
        "overlap_comm": true,
        "contiguous_gradients": true
    }
}
trainer = pl.Trainer( 
	accelerator='gpu', 
	strategy=pl.strategies.DeepSpeedStrategy(config="deepspeed_config.json"), 
)
더보기

-------------------------------------------------------

<deepspeed stage의미>

 

deepspeed엔 아래와 같은 다양한 stage방법을 통해 scaling이 가능합니다.

  • DeepSpeed ZeRO Stage 1 : optimizer states를 shard하면서도, 속도는 유지하고 per process 메모리 footprint를 줄입니다.
    ** full parameter sharding : local 연산에 필요한 model parameter, gradients, optimizers들의 부분집합만을 사용하게 하는 방법입니다. 
  • DeepSpeed ZeRO Stage 2 : optimizer states와 gradients를 shard하면서도, 속도는 유지하고 per process 메모리 footprint를 줄입니다.
  • DeepSpeed ZeRO Stage 2 Offload : optimizer states와 gradients를 CPU로 내려서 계산합니다. GPU에서 대부분의 메모리는 optimizer states에서 발생함에도 연산량은 적어, CPU로 내리면 GPU메모리를 절약할 수 있지만 예상하다시피 GPU-CPU transfer로 인한 communication latency가 발생합니다.
  • DeepSpeed ZeRO Stage 3 : optimizer states, gradients, parameters(, activation)를 shard하면서도, 속도는 유지하고 per process 메모리 footprint를 줄입니다.
  • DeepSpeed ZeRO Stage 3 Offload
  • DeepSpeed Activation Checkpointing : forward 동안 필요하지 않은 activation의 memory를 free하고, backward에서 필요할 때 다시 계산해서 사용합니다. 연산량은 늘지만 메모리를 많이 아낄 수 있습니다.

예를 들어 원래는 모든 optimizer, model parameters, gradients를 모든 GPU에 복사해서 사용하는데, 3stage의 경우 아래와 같이 process별로 partitioned되어 동작합니다.

[DeepSpeed 동작 https://towardsdatascience.com/pytorch-lightning-vs-deepspeed-vs-fsdp-vs-ffcv-vs-e0d6b2a95719]

 

-------------------------------------------------------


3. PyTorch Lightning for Multi-Node

 

위 설명 중간중간 눈치채셨겠지만, PyTorch Lightning으로 구현된 코드를 horovoddeepspeed를 활용해 Multi-Node로 실행이 가능합니다.

 

하지만 PyTorch Lightning 자체로도 아래와 같이 구현하면 multi-node에도 돌아가도록 해줄 수 있습니다.

 

PyTorch LightningPyTorch에서 이미 래핑이되어있으므로 PyTorch 모델 래핑, 데이터 샘플링,프로세스 초기화 등이 따로 필요 없으니, 몇가지 코드 변경사항과 실행방법에 대해 살펴보겠습니다.


a. Implementation

 

PyTorch Lightning에 구현하는 순서를 살펴보겠습니다.

 

1. PyTorch Lightning에 적용하기

Pytorch Lightning의 trainer에 들어갈 argument를 아래와 같이 설정해주어야합니다.

  1. num_nodes (변경 필요) : 전체 노드의 개수로 변경해주어야 합니다.
  2. batch_size (변경 필요 없음) : 여러개의 노드에서 실행하면 더 많은 batch를 적어주어야한다고 생각할 수도 있는데, 한개의 노드에서 4개의 batch가 최대였던 경우, 바꿔주지 않아도 각각의 노드에서 4batch로 실행됩니다.
    ex) 1node 4batch → 2nodes 4batch (8batch 수행)
  3. num_gpus (변경 필요 없음) : 각 노드에서 사용할 gpu의 개수로, 역시나 변경이 필요 없습니다.
num_gpus =8
num_nodes=2
batch_size=4
strategy = 'ddp_find_unused_parameters_true' if args.num_gpus > 1 else "auto" 

precision="16-mixed"

accum_batches=1
save_dir="./"
gradient_clip_val = 0.0

trainer = pl.Trainer(
        devices=num_gpus,
        num_nodes = num_nodes,
        batch_size = batch_size,
        accelerator="gpu",
        strategy=strategy,
        
        precision=precision,
        callbacks=[ckpt_callback, demo_callback, exc_callback, save_model_config_callback],
        
        accumulate_grad_batches=accum_batches, 
        default_root_dir=save_dir,
        gradient_clip_val=gradient_clip_val,
        
        logger=None,
        log_every_n_steps=1,
        max_epochs=10000000,
        reload_dataloaders_every_n_epochs = 0
    )

 


 

2. 멀티노드 실행 - master node

먼저 master node에서 아래와 같이 실행해줍니다.

  • NCCL_SOCKET_IFNAME : NIC를 명시해줍니다.
    ** PyTorch Lightning에서 기본적으로  NVIDIA GPU 간 통신을 최적화하기 위해 사용하는 DDP 백엔드는 NCCL입니다. 
  • MASTER_ADDR : 마스터 노드의 IP 주소
  • MASTER_PORT : 마스터 노드에서 데이터 통신에 사용할 포트 번호
  • WORLD_SIZE : 전체 "노드"의 개수
  • NODE_RANK : "노드"의 RANK로, MASTER 노드에서는 0을 주면 됩니다.
export NCCL_SOCKET_IFNAME=bond0 
export MASTER_ADDR="1.2.3.1"
export MASTER_PORT=6666
export WORLD_SIZE=3
export NODE_RANK=0 

python3 train.py \
    --dataset-config data.json \
    --model-config model.json \
    --num-gpus 8 \
    --batch-size 4 \
    --name my_train

** 코드 내부에서 아래와 같이 셋팅할 수도 있습니다.

os.environ['MASTER_ADDR']="1.2.3.1" 
os.environ['MASTER_PORT']=6666 
os.environ['WORLD_SIZE']=2 
os.environ['NODE_RANK']=0

print(os.environ.get('MASTER_ADDR'))
print(os.environ.get('MASTER_PORT'))
print(os.environ.get('WORLD_SIZE'))
print(os.environ.get('NODE_RANK'))

 


 

3. 멀티노드 실행 - slave nodes

 

slave node에서는 NODE_RANK를 바꾸어 실행해줍니다. 1.2.3.2와 1.2.3.3 두개에서 실행해주어야겠죠.

export NCCL_SOCKET_IFNAME=bond0 
export MASTER_ADDR="1.2.3.1"
export MASTER_PORT=6666
export WORLD_SIZE=3
export NODE_RANK=1 # or 2

python3 train.py \
    --dataset-config data.json \
    --model-config model.json \
    --num-gpus 8 \
    --batch-size 4 \
    --name my_train

b. Checklists

 

위를 구현하다 나오는 필자가 발견한 에러메시지들은 아래와 같습니다.

ImportError: /usr/local/lib/python3.10/dist-packages/torch/lib/libtorch_cuda.so: undefined symbol: ncclCommRegister
[c10d] The client socket has failed to connect to [::ffff:1.2.3.1]:6666 (errno: 110 - Connection timed out)

** port 연결이 안되어 있을 때 주로 나는 에러입니다.

The client socket has timed out after 1800s while trying to connect to (1.2.3.1(본인), 6666)
torch.distributed.DistNetworkError: Connection reset by peer
misc/socket.cc:484 NCCL WARN socketStartConnect: Connect to 1.2.3.1<33897> failed : Software caused connection abort

torch.distributed.DistBackendError: NCCL error in: ../torch/csrc/distributed/c10d/ProcessGroupNCCL.cpp:1970, unhandled system error (run with NCCL_DEBUG=INFO for details), NCCL version 2.20.5

ncclSystemError: System call (e.g. socket, malloc) or external library call failed or device error. socketStartConnect: Connect to 1.2.3.1<33897> failed : Software caused connection abort

 

위의 원인으로 간단하게는 아래와 같은 원인이 있을 수 있습니다.

  1. slave에서 ssh 데몬이 실행중이지 않거나 통신에 이상이 있을 때
  2. master 혹은 slave에서 pytorch 혹은 deepspeed가 설치되지 않았거나 버전차이 등 패키지의 문제가 있을 때
  3. docker 자체에 셋팅 문제가 있을 때

이를 위해 검토 해볼만한 것들을 차례대로 나열해보겠습니다.

 


 

Checklist 1. NCCL의 버전문제

 

아래와 같은 에러가 날 때의 주로 문제입니다.

torch 2.3.1 requires nvidia-nccl-cu12==2.20.5; platform_system == "Linux" and platform_machine == "x86_64", but you have nvidia-nccl-cu12 2.22.3 which is incompatible

 

NCCL의 버전에 문제가 없는지 확인해줍니다. 이를 위해 먼저 CUDANCCL의 버전을 확인해봅니다. 아래와 같이 시스템 자체에 설치된 nccl과 해당 nccl을 링크하고 있는 python의 nccl 두가지가 있습니다.

# CUDA
nvcc -V

# apt-get
dpkg -l | grep nccl

# Python
pip3 list | grep nccl
pip3 show nvidia-nccl-cu12

# Python Other method
python3 -c "import torch; print(torch.cuda.nccl.is_available(torch.randn(1).cuda()))"
python3 -c "import torch; print(torch.cuda.nccl.version())"
python3 -c "import torch; print(torch.cuda.nccl.__file__)"

 

libnccl2, libnccl-dev의 버전은 "2.15.5-1+cuda11.8" 버전 형태로 나올겁니다. 혹시나 안나온다면 아래 명령어로 설치해주고, cuda버전과 안맞다면 하기 링크를 통해 정확한 cuda버전에 맞는 nccl을 다시 설치해줍니다.

** https://developer.nvidia.com/nccl

apt-get update && apt-get install libnccl2 libnccl-dev


pip3로 설치된 nvidia-nccl-cu1X 또한 문제가 있는 경우 아래 명령어와 같이 다시 직접설치해줄 수도 있지만

pip3 install nvidia-nccl-cu12==2.22.3

 

PyTorch가 깔릴 때 같이 깔리는 패키지이므로 PyTorch를 지우고 다시 설치하는 것을 추천드립니다.

 

혹은 NCCL의 shared object의 link가 끊어져있을 수도 있습니다. 아래와 같이 locate를 활용해 nccl의 shared object 위치를 찾습니다.

apt-get install mlocate
locate nccl| grep 'libnccl.so'
/usr/lib/x86_64-linux-gnu/libnccl.so
/usr/lib/x86_64-linux-gnu/libnccl.so.2
/usr/lib/x86_64-linux-gnu/libnccl.so.2.17.1
더보기

---------------------------------------------------------

<shared object찾는 다른 방법 : ldconfig>

 

아래와 같이 ldconfig를 활용해 shared object를 찾을 수도 있습니다.

dpkg -l | grep nccl

dpkg -L libnccl-dev

ldconfig -v | grep "libnccl.so"

 

이후에 shared object의 경로를 /etc/ld.so.conf에 적어주고 ldconfig 하면, 이후에 리눅스 커널이 /etc/ld.co.conf에 있는 라이브러리 경로를 링크시켜주기도 합니다.

---------------------------------------------------------

해당 shared object의 링크를 확인해봅니다.

ldd /usr/lib/x86_64-linux-gnu/libnccl.so

 


 

Checklist 2. 포트확인

 

해당 포트가 이미 사용중이어서 연결이 안될 수도 있습니다. 아래와 같은 명령어로 확인합니다.

apt-get install lsof

lsof -i :6666

 

또 다른 방법으로 확인할 수도 있습니다.

# Method 1
apt-get install netcat

nc -zv 1.2.3.1 6666

# Method 2
apt-get install nmap

nmap -p 6666 1.2.3.1
#PORT      STATE SERVICE
#6666/tcp open  unknown

nmap -p 6666 localhost
#PORT      STATE  SERVICE\
#6666/tcp closed unknown

 


 

Checklist 3. docker 옵션 확인 : --privileged 

 

앞서 "—privileged=true" 옵션을 추가해야한다고 했는데, docker를 실행할 때 이와 같은 linux capability가 추가되어 있지 않을 수도 있습니다.

 

아래의 명령어로 확인해줍니다.

docker inspect CONTAINER_NAME | grep -i Capabilities

docker inspect --format='{{.HostConfig.Privileged}}' CONTAINER_NAME

 


 

Checklist 4. docker 옵션 확인 : --shm-size

 

도커 컨테이너 만들 때 "--shm-size=1024gb" 혹은 "--shm-size=1T"와 같이 주어야한다고 했는데, docker를 실행할 때 이와 같은 shared memory가 적절하게 설정되지 않아서 생기는 문제일 수도 있습니다.

 

아래의 명령어로 확인해줍니다.

docker inspect CONTAINER_NAME | grep -i shm

 

실제로 메모리가 부족한지를 확인하기 위해, 실시간으로 메모리를 확인해보고 싶을 때는 아래와 같은 명령어를 통해 확인합니다. 혹은 그냥 htoptop로 확인하셔도 됩니다.

# Method1
cat /proc/meminfo
apt install -y bsdmainutils
awk '$3=="kB"{$2=$2/1024;$3="MB"} 1' /proc/meminfo | column -t
awk '$3=="kB"{$2=$2/1024^2;$3="GB";} 1' /proc/meminfo | column -t
awk '$3=="kB"{if ($2>1024^2){$2=$2/1024^2;$3="GB";} else if ($2>1024){$2=$2/1024;$3="MB";}} 1' /proc/meminfo | column -t

# Method2
ipcs -a

# Method3
free -m

GCC버전 매칭 : https://dulidungsil.tistory.com/entry/GCC-%EB%B2%84%EC%A0%84%EA%B3%BC-C-%EB%B2%84%EC%A0%84-%EB%A7%A4%EC%B9%AD

OpenMPI 예시 : https://github.com/horovod/horovod/blob/master/docs/mpi.rst

OpenMPI 예시2 : https://horovod.readthedocs.io/en/stable/mpi.html

mpirun : https://www.open-mpi.org/doc/v3.0/man1/mpirun.1.php

DeepSpeed : https://velog.io/@nawnoes/PyTorch-Lightning-DeepSpeed

--privileged : https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities

backend : https://pytorch.org/docs/stable/distributed.html

lightningmodule : https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.core.LightningModule.html

 

 

728x90
반응형