Index
2026-09-07 ~ 09-08 — Progress

FurnitureBench 가변 길이 에피소드 length-bucketed packing 구현

lingbot-va | wan_va/dataset/packing.py (신규), wan_va/train.py, wan_va/modules/model.py

TL;DR

128
Effective Batch
16→4
Accum Steps
2999/3000
Step 완주

1 배경 / 목적

FurnitureBench all_low 데이터셋의 에피소드는 latent 프레임 수가 에피소드마다 다르다 (furniture_all_e128_pack 로그 실측: 103~362 프레임). PyTorch의 default_collate는 서로 다른 길이의 텐서를 하나의 배치 텐서로 쌓을 수 없어서, 기존 va_furniture_all_train_cfg.py는 "batch_size는 반드시 1이어야 한다"는 제약을 코드 주석으로 명시하고 있었다 ("the dataset returns whole episodes of variable latent length (120-166 frames), so default_collate cannot stack them").

그 결과 effective batch를 128까지 끌어올리는 유일한 수단이 gradient_accumulation_steps를 16까지 늘리는 것뿐이었다 (furniture_all_e128_train: batch 1 × accum 16 × 8 GPU = 128). 매 micro-step이 배치 크기 1짜리 forward/backward라, GPU 한 번에 처리하는 텐서가 작아 실제 배치 병렬화 이득 없이 16번을 순차적으로 도는 구조였다.

기존 한계: batch_size=1 고정 → GPU가 한 번에 에피소드 하나만 처리, accum만 늘려서는 커널 하나당 연산량이 늘지 않아 배치화의 실질적 이득(커널 launch 오버헤드 상각, 텐서 코어 활용도 상승)을 못 얻음.

2 작업 내용

변수 길이 에피소드를 실제로 배치화하기 위해 3개 파일에 걸쳐 packing 메커니즘을 새로 구현했다.

신규 파일: wan_va/dataset/packing.py

episode_latent_frames(dataset) - 메타데이터만으로 F = (n_video_frames - 1) // 4 + 1 계산 (latent 파일을 열지 않음) LengthBucketBatchSampler(lengths, batch_size, rank, world_size, seed=42, pool_factor=50) - epoch마다 seed+epoch로 셔플 → rank별 균등 샤딩(tail padding, DistributedSampler 방식) - shard를 batch_size*pool_factor 크기의 pool로 나눠 pool 내부만 길이순 정렬 (전체 정렬 대신 pool 단위 정렬 = 배치 내 길이 편차를 줄이되 매 epoch 순서 무작위성 유지) - pool을 batch_size 단위로 잘라 배치 생성 후 배치 순서 셔플 - 모든 rank가 동일한 배치 수를 갖도록 보장 (FSDP 요구사항) make_pad_collate(pad_frames_multiple=8) - 배치 내 latents/actions/actions_mask를 F축 기준 round_up(max_F, 8)까지 zero-pad - valid_frames (B,) 텐서를 함께 반환 → 원래 프레임 수 기록

wan_va/train.py

config.batch_size > 1일 때만 packing 경로로 분기(기존 batch=1 경로는 그대로 유지, 하위 호환). compute_loss에서 valid_frames로부터 frame_valid 마스크를 만들어 latent_loss / action_loss 평균 계산에서 padding 프레임을 제외했다.

frame_valid = (arange(Fn)[None,:] < valid_frames[:,None]).flatten() latent_loss = ((latent_loss_per_frame / mask_per_frame) * frame_valid).sum() / frame_valid.sum()

추가로 LINGBOT_PROFILE=<n> 환경변수 기반 프로파일링 훅을 넣었다 — micro-step당 video/action/text 토큰 수, fwd+bwd 시간(s), peak memory(GB)를 출력하고, 마지막 profiled micro-step에서는 torch.profiler로 self-time 기준 top-14 CUDA 커널 테이블을 출력한다. packing 도입 후 실제로 어떤 attention 커널(FlexAttention/Triton vs Flash)이 선택되는지, GPU가 얼마나 활용되는지 확인하기 위한 도구다.

wan_va/modules/model.py — FlexAttnFunc.init_mask

packing의 정확성은 attention 마스킹에 달려 있다. valid_frames 인자를 추가해, padding 프레임에 해당하는 토큰의 seq_id를 -1로 설정 — FlexAttention이 동일 seq_id끼리만 attend하는 구조이므로, -1인 토큰은 query·key 양쪽에서 실질적으로 attention 밖으로 밀려난다(다른 에피소드로의 leak도 함께 차단).

lat_valid = arange(L_F // patch_size[0])[...] * patch_size[0] < valid_frames[...] latent_seq_id = where(lat_valid, latent_seq_id, -1) # action_seq_id 동일 처리

신규 config: va_furniture_all_e128_pack_cfg.py

batch_size=4/GPU × accum=4 × 8 GPU = effective 128 — 기존 furniture_all_e128_train (batch 1 × accum 16 × 8 GPU = 128)과 동일한 effective batch를 유지하되, 이번엔 GPU당 4개 에피소드를 실제로 한 forward에 묶어 처리한다. pad_frames_multiple=8, bucket_pool_factor=50는 config 기본값.

3 결과 (수치)

RunGPUbatch × accumEff. Batchs/it (실측)비고
furniture_all_train (no-pack)41 × 104019.34sstep 7500 부근, 기존 baseline
furniture_all_e128_pack (pack)84 × 412824.26~24.32sstep ~1800 부근, packing 적용

학습 로그로 packing 메커니즘 자체는 정상 동작이 확인됐다: "packing: batch 4/GPU, pad_frames_multiple=8, 40 batches/epoch/rank, latent F range 103-362". furniture_all_e128_pack은 preemption으로 두 차례(1z8ijh3z → hp3o4tam) 재개된 끝에 step 2999/3000까지 완주했고, 최종 wandb summary는 avg_action_loss 0.0112 / avg_video_loss 0.0616 / grad_norm 0.12 (누적 runtime ≈ 8.3시간, requeue 구간 포함).

정확성 확인: valid_frames 마스킹이 attention·loss 양쪽에 정확히 전파되어, padding된 프레임이 구조적으로 gradient에 기여하지 않는다는 것은 코드 레벨에서 보장됨.
처리량 개선 미확인: GPU 8장(packed)의 s/it가 GPU 4장(non-pack)보다 오히려 높다. GPU 수가 달라 순수 비교는 아니지만, 최소한 "packing으로 accum을 16→4로 줄였으니 step당 시간이 준다"는 기대는 이번 실측만으로는 확인되지 않았다 — pad_frames_multiple 단위(8 latent frame)마다 F 조합이 바뀔 때 FlexAttention/inductor가 매번 recompile(코드 주석 근거: 4~6초/회)되는 오버헤드가 유력한 후보이나, 아직 LINGBOT_PROFILE을 실제로 돌려 확인하지는 못했다.

4 Takeaway

의미

packing은 "batch_size=1 강제"라는 근본 제약을 풀어 진짜 배치 병렬화를 가능하게 한 인프라 개선이고, attention·loss 마스킹의 정확성도 코드 구조로 보장됐다. 다만 이번 구현에서 "같은 effective batch를 더 적은 micro-step으로 달성하면 학습이 빨라질 것"이라는 가설은 아직 뒷받침되지 않았다 — accum 감소(16→4)로 얻은 이득이 packing 자체의 오버헤드(길이 편차가 큰 pool 정렬, F 변경마다의 recompile)에 상쇄됐을 가능성이 있다. 즉 packing은 "필요조건"(가변 길이를 실배치로 처리 가능)은 만족시켰지만, "충분조건"(처리량 개선)은 아직 증명되지 않은 상태다.

5 Next Steps

미해결 한계

packed run(8GPU)과 non-pack baseline(4GPU)의 GPU 수가 달라 s/it 비교가 confound됨 — 같은 GPU 수(8) 조건에서 furniture_all_e128_train(non-pack, 8GPU)과 furniture_all_e128_pack(pack, 8GPU)의 s/it를 직접 비교한 데이터가 로컬 로그에 없다.

다음 실험

(1) 동일 GPU 수(8)에서 non-pack e128_train vs pack e128_pack의 s/it를 직접 비교. (2) LINGBOT_PROFILE=3으로 실제 프로파일을 떠서 top CUDA 커널이 FlexAttention/Triton인지 Flash인지, GPU peak memory와 fwd+bwd 시간을 확인. (3) pad_frames_multiple을 8→16/32로 늘려 F 조합 recompile 빈도 자체를 낮췄을 때 s/it 변화 관찰.