← 목록으로
gcube 레플리카로 LLM 분산 파인튜닝하기: Axolotl + DeepSpeed 멀티노드 실전 가이드
gcube · hands-on

gcube 레플리카로 LLM 분산 파인튜닝하기: Axolotl + DeepSpeed 멀티노드 실전 가이드

on this page

RTX 5090 컨테이너 2대를 묶어서 실제로 분산 파인튜닝이 되는지 처음부터 끝까지 라이브로 검증해봤습니다. 순탄하게 흘러가지 않았습니다 — NCCL 통신이 완전히 멈추는 문제까지 만났고, 원인을 찾아 우회하는 과정 자체가 이 글의 핵심입니다.

왜 멀티노드인가

단일 GPU로 파인튜닝을 시리즈로 다뤄왔지만, 실제 현업에서는 모델이 커지거나 데이터셋이 늘어나는 순간 단일 GPU의 VRAM과 처리량으로는 감당이 안 되는 지점이 옵니다. 이때 선택지는 두 가지입니다 — 더 큰 GPU 한 장을 쓰거나, 같은 GPU를 여러 장 묶어 나눠 계산하거나. 후자가 바로 분산 학습(distributed training)이고, gcube의 레플리카 기능이 정확히 이 시나리오를 위한 것입니다.

이번 글에서는 gcube 레플리카로 실제 멀티노드 분산 학습이 되는지, 노드를 늘렸을 때 정말로 학습 속도나 처리 가능한 규모가 확장되는지를 라이브로 검증했습니다. 동일 스펙 GPU를 레플리카로 여러 대 묶는 방식이 이종 GPU를 섞는 것보다 연산 속도·VRAM이 맞아떨어져 실전에서 훨씬 안정적입니다. 이번 테스트는 gcube 콘솔에서 Tier2 RTX5090 × 1 / 32GB 컨테이너를 레플리카 2개로 배포해, 실제로 노드 간 통신이 성립하고 분산 학습이 완주되는지 검증했습니다.

환경 구성

컨테이너 이미지: axolotlai/axolotl-cloud-uv:main-py3.11-cu130-2.9.1

Axolotl 공식 이미지 중에서도 -cloud-uv 계열을 선택한 이유는 두 가지입니다.

  • cloud: RunPod류 GPU 클라우드 배포에 맞춰 JupyterLab이 기본 실행되고, HF 캐시 볼륨 마운트 등이 사전 구성되어 있음
  • uv: uv 패키지 매니저 기반 빌드로, RTX 5090(Blackwell, sm_120) 대응이 가장 안정적

컨테이너 설정

항목
GPUTier2 RTX5090 × 1 / 32GB
레플리카2
포트8888(Jupyter), 29500(분산 통신)
환경변수HF_HOME=/data/.cache/huggingface

배포 후 확인된 스펙: CUDA 13.0, Driver 580.126.09, torch 2.9.1+cu130 — 두 노드 모두 동일하게 검증됨.

1차 시도: hang에 부딪히다

첫 배포에서 노드 간 기본 네트워크(TCP raw socket)는 정상이었습니다. 직접 소켓을 열어 데이터를 주고받는 테스트까지 성공했죠. 문제는 그 다음이었습니다.

accelerate launch --config_file accelerate_config.yaml \
  -m axolotl.cli.train examples/llama-3/lora-1b.yml --num_epochs 1

이 명령을 양쪽 노드에서 실행하자, rank 1(node2)은 데이터셋 준비를 마치고 barrier에서 대기 중인데 rank 0(node1)은 8분 넘게 CPU 시간이 전혀 늘지 않는 완전한 hang 상태에 빠졌습니다.

ss -tnp로 확인한 소켓 상태가 결정적 단서였습니다.

CLOSE-WAIT   0   10.0.9.126:xxxxx   10.0.10.154:xxxxx   (6개의 서로 다른 임시 포트)

node1이 node2를 향해 연 여러 개의 임시 포트가 전부 CLOSE-WAIT 상태로 남아있었습니다. NCCL/torch.distributed의 bootstrap(rendezvous) 단계에서 두 노드 간 소켓 핸드셰이크가 꼬인 것으로 보였고, 격리 테스트(axolotl 없이 순수 torch.distributed all_reduce)를 준비하는 도중 웹 콘솔 터미널 세션 자체가 끊어지며 디버깅이 중단됐습니다.

교훈: 재현 안 되는 일시적 네트워크 이슈일 가능성이 높다는 점, 그리고 문제가 생기면 axolotl 전체를 다시 돌리기보다 최소 단위(순수 NCCL all_reduce)로 먼저 격리하는 게 훨씬 빠른 진단 방법이라는 점입니다.

2차 시도: 원인 격리와 설정 이슈 해결

동일한 스펙으로 컨테이너를 새로 배포한 뒤, 이번엔 axolotl을 바로 돌리지 않고 순수 NCCL 통신 테스트부터 실행했습니다.

import torch, torch.distributed as dist, datetime, time
t0 = time.time()
dist.init_process_group(backend='nccl', timeout=datetime.timedelta(seconds=60))
print('init done in', time.time()-t0, 's. rank', dist.get_rank(), 'world_size', dist.get_world_size())
torch.cuda.set_device(0)
x = torch.ones(1).cuda() * (dist.get_rank()+1)
dist.all_reduce(x)
print('after all_reduce', x)
dist.destroy_process_group()
# node1 (rank 0)
MASTER_ADDR=10.0.9.59 MASTER_PORT=29500 WORLD_SIZE=2 RANK=0 LOCAL_RANK=0 \
NCCL_SOCKET_IFNAME=eth0 NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1 NCCL_DEBUG=WARN \
python3 nccl_test.py

# node2 (rank 1) — 동일 명령에 RANK=1

결과:

init done in 3.30 s. rank 0 world_size 2
before all_reduce tensor([1.])
NCCL version 2.27.7+cuda13.0
after all_reduce tensor([3.])   # 1(rank0) + 2(rank1) = 3, 정확히 통신됨
DONE OK

양쪽 랭크 모두 3초 이내에 통신이 성립했습니다. 1차 시도의 hang은 일시적인 이슈였다는 게 확인된 셈입니다.

남은 설정 이슈 3가지

NCCL 통신이 확인됐다고 바로 학습이 도는 건 아니었습니다. accelerate + deepspeed 조합에서 다음 세 가지 설정 이슈를 순서대로 만났고, 학습 도중 참고할 만한 운영 팁도 하나 있었습니다.

1) 기본 launcher(pdsh)는 SSH가 필요하다

subprocess.CalledProcessError: Command ['deepspeed', '--hostfile', 'None', ..., '--launcher', 'pdsh', ...]

DeepSpeed의 기본 멀티노드 launcher는 rank 0가 다른 노드에 SSH로 직접 접속해 프로세스를 원격 실행하는 구조입니다. gcube의 axolotl-cloud 이미지는 PUBLIC_KEY/SSH_KEY 환경변수가 없으면 SSH 데몬 자체가 뜨지 않습니다. 해결은 launcher를 SSH 불필요 방식으로 바꾸는 것이었습니다.

deepspeed_config:
deepspeed_multinode_launcher: standard

standard launcher는 각 노드가 독립적으로 프로세스를 띄우고 main_process_ip:main_process_port로만 서로를 찾습니다. SSH가 전혀 필요 없습니다.

2) deepspeed_config_file 사용 시 accelerate 최상위 키와 충돌

ValueError: When using `deepspeed_config_file`, the following accelerate config variables will
be ignored: ['gradient_accumulation_steps', ..., 'mixed_precision']. Please specify them
appropriately in the DeepSpeed config file.

deepspeed_config_file(json)로 세부 설정을 넘길 때는 accelerate_config.yaml 최상위에 mixed_precision 같은 값을 같이 넣으면 안 됩니다. bf16 여부는 axolotl 학습 config(bf16: true) 쪽에서 이미 처리되므로 최상위 키를 제거해야 합니다.

3) 파일명 오타 — zero2_bf16.json은 존재하지 않는다

ValueError: Expected a string path to an existing deepspeed config... Received: deepspeed_configs/zero2_bf16.json

axolotl fetch deepspeed_configs로 받아지는 실제 파일은 zero1.json, zero2.json, zero3.json, zero3_bf16.json 등입니다. zero2 계열에는 _bf16 버전이 없습니다. bf16은 어차피 학습 config에서 지정되므로 그냥 zero2.json을 쓰면 됩니다.

최종 accelerate_config.yaml

compute_environment: LOCAL_MACHINE
distributed_type: DEEPSPEED
num_machines:2
num_processes:2
machine_rank:0          # node2는 1
main_process_ip:10.0.9.59
main_process_port:29500
same_network:true
rdzv_backend: static
deepspeed_config:
deepspeed_config_file: deepspeed_configs/zero2.json
zero3_init_flag:false
deepspeed_multinode_launcher: standard

참고: 웹 콘솔 터미널이 끊겨도 학습은 죽지 않는다

학습 도중(69% 지점) 웹 터미널 세션이 “연결이 종료되었습니다” 메시지와 함께 끊긴 적이 있습니다. 페이지를 새로고침해 재접속했더니 — tmux 세션 안에서 학습이 그대로 진행되고 있었습니다. 학습 프로세스는 브라우저의 웹소켓 연결과 독립적으로 동작하기 때문에, 콘솔이 끊겨도 당황할 필요 없이 새로고침만 하면 됩니다.

최종 결과: 학습 완주와 파인튜닝 검증

실행 및 완주 로그

세 가지 설정을 모두 반영한 뒤, 양쪽 노드에서 NCCL 환경변수를 다시 export하고 학습을 실행했습니다.

export NCCL_SOCKET_IFNAME=eth0 NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1

accelerate launch --config_file accelerate_config.yaml \
  -m axolotl.cli.train examples/llama-3/lora-1b.yml --num_epochs 1
[axolotl.utils.samplers.multipack] gather_len_batches: [232, 232]   # 두 랭크 동기화 확인
[axolotl.utils.data.sft] Maximum number of steps set at 519
trainable params: 11,272,192 || all params: 1,247,086,592 || trainable%: 0.9039

100%|██████████| 519/519 [28:15<00:00, 3.27s/it]
{'train_loss': '1.207', 'eval_loss': '1.18', 'eval_ppl': '3.255'}
Training completed! Model successfully saved to ./outputs/lora-out

Llama-3.2-1B + LoRA를 RTX5090 2노드로 519 step, 약 28분에 완주했습니다. GPU당 메모리 사용량은 8.49GB 수준으로, 32GB 중 여유가 충분히 남아 더 큰 배치나 더 큰 모델로 확장할 여지가 있습니다.

Before / After 비교로 확인하기

숫자(loss 1.4 → 1.2)만으로는 체감이 안 되니, 같은 질문에 대한 base 모델과 LoRA 적용 모델의 답변을 직접 비교했습니다.

질문: “Explain what a black hole is in simple terms.”

Fine-tuning 전 (base 모델)

A black hole is a region of space where gravity is so strong that nothing, not even light, can escape. The gravity of a black hole is so strong that even light cannot escape. The gravity of a black hole is so strong that even light cannot escape. (동일 문장 무한 반복)

Fine-tuning 후 (LoRA 적용)

A black hole is a region of space where gravity is so strong that nothing, not even light, can escape. It is a region of space where the escape velocity from the surface of the object is greater than the speed of light. This means that nothing, not even light, can escape from the black hole. The name “black hole” comes from the fact that the object is invisible to the naked eye, and the only way to see it is through the effects of gravity.

1B 파라미터급 소형 모델 특유의 반복 생성(degenerate repetition) 문제가, 단 519 step의 파인튜닝만으로 눈에 띄게 개선된 걸 확인할 수 있습니다. 탈출속도 개념과 이름의 유래까지 자연스럽게 이어지는 답변으로 바뀌었죠. loss 하락이 만들어낸 실질적 변화입니다.

정리: 멀티노드 트러블슈팅 체크리스트

  1. 컨테이너 이미지: Blackwell(RTX 50 시리즈) GPU는 cloud-uv 계열 + 최신 cuXXX 태그 확인
  2. 네트워크 확인 순서: raw TCP 소켓 → 순수 NCCL all_reduce → axolotl 전체 실행 (역순으로 디버깅하면 문제 범위를 넓혀서 시간 낭비)
  3. deepspeed_multinode_launcher: standard 필수 — SSH 데몬이 없는 컨테이너 환경 전제
  4. deepspeed_config_file 사용 시 accelerate 최상위 키(****mixed_precision 등) 제거
  5. zero2 config는 zero2.json (bf16 버전 없음, 학습 config에서 별도 지정)
  6. 웹 콘솔이 끊겨도 tmux 세션의 학습은 안전 — 새로고침 후 이어서 확인
  7. hang이 발생하면 원인이 재현 가능한 설정 문제인지, 일시적 네트워크 이슈인지부터 격리 테스트로 구분

이 글의 모든 로그와 결과는 gcube RTX 5090 레플리카 컨테이너에서 실시간으로 검증됐습니다.