전체 글 304

save_state는 가중치 저장이 아니라 재개용 폴더 조립이다

같은 스크립트, 같은 프로세스 수, 같은 래퍼 그래프를 다시 세운 뒤에야 그 폴더가 의미가 있다. 배포용 가중치는 다른 함수다.Accelerator.save_state의 독스트링은 용도를 한 문장으로 제한한다. 학습 중 체크포인트를 남기고, 같은 환경에서 상태를 되돌릴 때 쓰라는 것이다. save_model과 get_state_dict는 그 반대 방향이다. 래퍼를 걷어 낸(또는 샤드를 모아 만든) 가중치를 다른 프로세스, 다른 스크립트, 심지어 단일 GPU 추론으로 가져가기 위한 export다. 폴더 안에 model.safetensors가 있다고 해서 save_state가 export인 것은 아니다. 그 옆의 optimizer.bin, scaler.pt, random_states_{rank}.pkl, 그..

wait_for_everyone과 gather는 그래디언트 통신이 아니다

분산 학습에서 프로세스가 멈춰 서는 순간은 대개 backward가 아니라, 한 rank만 파일을 쓰고 나머지가 all_gather를 기다리는 그 지점이다.Hugging Face Accelerate를 처음 붙이면 wait_for_everyone, gather, reduce가 학습 루프의 “통신 API”처럼 보인다. 앞에서 DDP·FSDP·ZeRO를 읽었다면 그 직감이 틀린 이유를 이미 알고 있다. 그래디언트 통신은 prepare가 씌운 래퍼의 backward() 안에서 자동으로 일어난다. PartialState.wait_for_everyone과 accelerate.utils.operations의 collective는 그 채널이 아니다. 평가 점수를 모으고, 로그용 loss를 맞추고, rank 0만 디스크를..

fp16만 GradScaler를 쓰는 이유, 그리고 DeepSpeed가 `_mixed_precision`을 no로 두는 이유

왜 스케일러가 있는가IEEE 754 이진 부동소수점은 부호 1비트, 지수, 가수로 나뉩니다. 문제가 되는 쪽은 지수입니다.fp16은 지수 5비트입니다. 표현 범위는 대략 6e-8에서 65504입니다. 정규화된 최솟값이 2^-14 ≈ 6.10e-5이고, 비정규화까지 내려가면 2^-24 ≈ 5.96e-8 근처입니다. 학습 후반의 그래디언트는 이 바닥보다 작은 경우가 흔합니다. 그래디언트가 전부 0으로 내려가면 가중치는 멈춥니다. 반대로 한 번이라도 65504를 넘으면 Inf가 되고, 그 뒤로는 NaN입니다.그래서 fp16 학습은 손실에 큰 수를 곱합니다. 그래디언트가 표현 가능한 구간으로 올라오고, optimizer.step 직전에 같은 수로 나눕니다. 이게 loss scaling입니다. 스케일이 너무 크면..

accelerate launch는 엔진을 안 띄운다. 좌표만 심는다

launch가 고르는 것은 런처이지, DistributedType이 아니다진입점은 commands/launch.py의 launch_command입니다. _validate_launch_command가 YAML과 CLI를 한 args로 합친 뒤, 분기는 대략 이렇게 생깁니다.if args.use_deepspeed and not args.cpu: deepspeed_launcher(args)elif args.use_fsdp and not args.cpu: multi_gpu_launcher(args)elif args.use_megatron_lm and not args.cpu: multi_gpu_launcher(args)elif args.multi_gpu and not args.cpu: mul..

FSDP1은 뭉개고 FSDP2는 파라미터마다 자른다

같은 일, 다른 그릇두 버전 모두 하는 일은 ZeRO-3와 같다. forward 전에 파라미터를 all-gather하고, 연산이 끝나면 필요하면 다시 샤드하고, backward 뒤에 그래디언트를 reduce-scatter한다. optimizer state는 로컬 샤드만 들고 간다. 메모리 절감의 원천은 통신이지, 마법이 아니다.갈리는 지점은 무엇을 하나의 통신 단위로 묶는가다. FSDP1은 모듈 하나를 긴 1차원 텐서로 만든다. FSDP2는 파라미터 하나를 하나의 DTensor로 만든다. 그 선택이 dtype, requires_grad, LoRA, fp8, 체크포인트 키, 그리고 optimizer가 붙잡는 Python 객체까지 결정한다.Accelerate 문서의 그림은 레이어 하나 안에 Linear 세 ..

FSDP2 `prepare`는 파라미터 정체성을 갈아끼운다

Hugging Face 스크립트는 optimizer = AdamW(model.parameters())를 먼저 쓰고, FSDP2의 fully_shard는 그 파라미터를 새 nn.Parameter(DTensor)로 바꾼다. Python id도 스토리지도 달라진다. Accelerate가 존재하는 이유의 절반은, 이 틈을 학습이 깨지기 전에 메우는 일이다.이 글은 Hugging Face Accelerate의 FSDP2 경로를 소스 기준으로 읽는다. 대상은 src/accelerate/accelerator.py의 _prepare_fsdp2, src/accelerate/utils/fsdp_utils.py의 fsdp2_prepare_model / fsdp2_switch_optimizer_parameters, 그리고 P..

Accelerate가 실제로 소유하는 계약 — Optimizer, Scheduler, DataLoader wrapper

공유 버스: GradientState와 _do_syncGradientState는 state.py에 있다. Accelerator마다 하나씩 있는 것처럼 보여도, 내부는 SharedDict다. 같은 프로세스 안에서는 하나의 sync_gradients가 모든 wrapper에 보인다.학습 루프가 매 배치마다 들어가는 문은 Accelerator.accumulate다.with accelerator.accumulate(model): outputs = model(inputs) loss = loss_fn(outputs) accelerator.backward(loss) optimizer.step() scheduler.step() optimizer.zero_grad()accumulate의 첫..

Accelerator.prepare는 wrap이 아니라 객체 그래프 조립이다

한 줄 API처럼 보이지만, prepare는 모델·옵티마이저·스케줄러·데이터로더가 같은 파라미터 객체와 같은 스텝 경계를 가리키게 만드는 조립기다. wrap은 그 조립의 마지막 한 단계일 뿐이다.Hugging Face Accelerate를 처음 만지면 학습 스크립트는 거의 한 줄로 바뀐다. Accelerator()를 만들고, 네 객체를 prepare에 넘기고, 루프는 그대로 둔다. 공식 예제 examples/nlp_example.py도 그렇게 한다.model, optimizer, train_dataloader, eval_dataloader, lr_scheduler = accelerator.prepare( model, optimizer, train_dataloader, eval_dataloader, ..

ZeRO-2/3에서 no_sync를 안 쓰는 이유 — reduce-scatter는 파티션 자체다

이전 글의 문장을 한 번 더 꺼내 보자. DDP에서 그래디언트 동기화는 옵티마이저가 아니라 autograd hook이다. 그래서 gradient accumulation의 중간 미니배치는 no_sync로 AllReduce를 건너뛰고, 경계에서 한 번만 평균을 맞춘다. 그 패턴은 “통신은 비싸고, 업데이트 직전에만 전 랭크가 같은 그래디언트를 가지면 된다”는 가정 위에 서 있다.ZeRO-2는 그 가정을 버린다. 각 랭크는 애초에 전체 평균 그래디언트를 들고 있지 않다. 들고 있는 것은 자신이 맡은 조각뿐이다. 그 조각을 만드는 연산이 reduce-scatter다. 스위치를 끄면 파티션이 생기지 않는다.이 글은 AllReduce와 Reduce-Scatter의 차이에서 시작해, ZeRO 스테이지가 무엇을 쪼개는..

DDP는 optimizer.step이 아니라 backward에서 통신한다

분산 학습에서 프로세스 간 통신은 optimizer.step()이 아니라 backward()의 AllReduce에서 일어난다. 그래디언트 누적은 그 사실을 모르는 DDP 훅 때문에 중간 배치에서도 통신을 일으키고, 특히 멀티노드에서 배치당 시간을 두 배 가까이 낭비한다. Accelerate의 no_sync와 accumulate()는 그 훅을 끄는 장치이지, 옵티마이저 스텝을 건너뛰는 장치가 아니다.결론부터docs/source/concept_guides/gradient_synchronization.md가 말하는 핵심은 한 문장이다. PyTorch DDP는 모델의 forward와 backward에 트리거 포인트를 심고, 그래디언트 동기화는 backward의 AllReduce에서 일어난다. optimizer...