전체 글 305

macOS에서 멀티프로세싱(fork) 수행 중 강제 abort가 날 때 해결법

macOS(High Sierra 이상)는 자식 프로세스가 fork()될 때 Objective-C 초기화 메서드가 다른 스레드에서 실행 중이면 안전상의 이유로 프로그램을 즉시 강제 종료(abort)시킨다. 이로 인해 Python, PHP, Ruby 등 C 확장이나 Objective-C 기반 라이브러리(libcurl, pg 젬 등)를 쓰면서 멀티스레드/멀티프로세스 작업을 할 때 objc[...]: +[...] may have been in progress in another thread when fork() was called 오류와 함께 프로그램이 멈추거나 죽는 현상이 발생하게 된다. 대표적으로 Ansible, Python 멀티프로세싱, Ruby on Rails(Solid Queue), PHP 등을 mac..

MacOS 2026.09.14

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 스테이지가 무엇을 쪼개는..