S3 데이터 스트리밍을 통한 대규모 GPU 학습 가속화: PB급 데이터에의 확장과 선행 읽기 설계
요약
본 글은 대규모 GPU 학습 환경에서 S3 스토리지의 데이터를 직접 읽어 들여, 기존 클러스터 스테이징 방식의 한계를 극복하는 방법을 제시합니다. 데이터셋을 로컬 스토리지에 복사할 필요가 없어지면서 PB급 데이터 사용량 제한과 복사 시간을 해결했습니다. 핵심은 선행 배치(pre-batch)를 미리 읽어와 I/O 지연 문제를 숨기는 것입니다.
핵심 포인트
- S3 직접 읽기 구현으로 학습 데이터 용량의 상한선을 제거함.
- 클러스터 스토리지 스테이징 과정과 대기 시간을 완전히 없앰.
- 선행 배치(pre-batch) 기법을 활용하여 I/O 지연 문제를 해결함.

서론
Turing의 mlops 3 팀 소속 카토입니다. 2026년 3월에 Turing에 입사하여 자율주행 모델 학습 기반 개발 워크플로우 개선을 담당하고 있습니다. 최근에는 주로 학습 속도 가속화에 집중해 왔습니다.
이번 글에서는 학습 시 S3에서 데이터를 직접 읽어 들임으로써, 학습 데이터 용량의 제한과 클러스터로 학습 데이터를 스테이징(staging)하는 과정을 없앤 경험에 대해 이야기하고자 합니다.
이 글의 내용
- 학습 데이터를 S3에서 직접 읽는 구성으로 변경하여, 학습에 사용할 수 있는 데이터량을 클러스터 스토리지 용량으로부터 분리했습니다.
- S3의 대역폭은 충분했지만, 한 번의 읽기 작업에 걸리는 시간이 과제였습니다.
- 선행 배치(batch)를 미리 읽어 옴으로써 이 시간을 숨겼습니다. 그 결과, 스토리지 용량 상한이나 복사 대기 시간 없이 S3에 있는 데이터를 그대로 학습에 사용할 수 있게 되었습니다.
1. 배경과 과제
기존 구성
지금까지는 학습을 시작하기 전에 S3의 데이터셋을 클러스터 측 스토리지로 복사했습니다. 복사 대상은 공유 파일 시스템인 FSx for Lustre이거나, 각 노드의 로컬 NVMe였습니다. 이 글에서 이 복사 과정을 스테이징이라고 부릅니다.

기존 구성
당시의 구성은 이전 글에서도 소개된 바 있습니다.
과제
Turing은 이미 PB급 학습 데이터를 보유하고 있습니다. 반면, 클러스터 측 스토리지에 배치할 수 있는 것은 그 극히 일부에 불과합니다. 지금까지는 학습에 필요한 만큼만 스테이징했기 때문에 당장 큰 문제는 없었습니다. 하지만 학습 데이터는 계속 증가하고 있고, 더 많은 데이터로 학습하고 싶은 상황도 늘어나고 있습니다. 이대로 가다가는 다음 문제에 직면할 것이 보였습니다. 그래서 문제가 발생하기 전에 대책을 검토했습니다.
스토리지 용량이 허용하는 만큼만 학습 데이터를 늘릴 수 있다. 학습에 사용할 데이터셋은 통째로 클러스터 측 스토리지에 들어 있어야 합니다. S3에는 얼마든지 많은 데이터를 저장할 수 있지만, 학습에 사용 가능한 것은 스토리지에 담길 수 있는 양뿐입니다. 한 번의 학습으로 사용할 수 있는 데이터는 스토리지 용량에 맞춰 제한됩니다.
스토리지 증설은 쉽지 않다. 공유 파일 시스템은 확보한 용량만큼 비용이 발생합니다. 사용하지 않는 시간에도 동일합니다. 로컬 NVMe의 용량은 인스턴스 종류에 따라 결정되어 늘릴 수 없습니다.
여유 공간 관리가 복잡해진다. 새로운 데이터셋을 배치하려면, 오래된 것을 지우고 빈 공간을 만들어야 합니다. 여러 사람이 각기 다른 데이터셋으로 학습하면 조정이 필요합니다. 데이터가 많아질수록 이 번거로움도 커집니다.
게다가 스테이징에는 시간이 걸립니다. 데이터셋 복사가 완료될 때까지 학습을 시작할 수 없습니다. 이 대기 시간 역시 데이터셋이 커질수록 길어집니다.
목표
스테이징 속도를 높여도 용량의 상한선은 변하지 않습니다. 스토리지를 늘려도, 데이터가 증가하면 다시 같은 문제에 직면합니다. 그래서 클러스터 측에 데이터를 배치하는 것 자체를 없애기로 했습니다.
목표는 학습에 사용할 수 있는 데이터량을 클러스터 스토리지 용량으로부터 분리하는 것입니다. 원본 데이터는 S3에 있습니다. 학습이 그곳에서 직접 읽힐 수만 있다면, 클러스터 측에 복사본을 가질 필요가 없어집니다. 용량의 상한선이 사라지고, 스테이징 대기 시간도 동시에 사라집니다.
2. S3에서 직접 읽는 구성
전체 개요
새로운 구성에서는 학습 작업(job)이 S3에서 데이터를 직접 읽습니다. Lustre나 NVMe에 데이터셋을 배치할 필요가 없습니다.
!
현재 구성
Range GET
학습 데이터의 대부분은 차량 카메라 영상입니다. Turing의 차량에는 7대의 카메라가 있습니다. 한 번의 학습 스텝에서 사용하는 것은 긴 영상 중 극히 일부 프레임에 불과합니다.
S3에는 객체의 일부만 가져올 수 있는 Range GET이라는 기능이 있습니다. 이것을 사용하여 필요한 프레임을 포함하는 부분만 읽습니다. 비디오 파일을 통째로 다운로드할 필요가 없습니다.
S3에서는 파일을 열고 읽는 위치까지 이동한다는 개념의 작업은 없습니다. 대신, 가져오는 요청에
Range GET을 사용하려면, '원하는 프레임이 파일의 몇 바이트째에 있는지'를 읽기 전에 알고 있어야 합니다.
영상은 프레임별로 독립적으로 압축되어 있는 것이 아닙니다. 특정 프레임을 복원하려면 그 앞쪽에 있는 경계(区切り) 프레임부터 순서대로 디코딩해야 합니다. 즉, 경계 프레임이 파일의 어디에 있는지 알면 읽어야 할 범위가 결정됩니다.
그래서 영상 파일마다 작은 인덱스를 미리 만들었습니다. 내용은 경계 프레임의 위치와 디코딩에 필요한 파라미터입니다. 파일당 약 10 KB 정도이며, 이 역시 S3에 보관하고 있습니다.
학습 시의 흐름은 다음과 같습니다.
- 사용할 프레임 번호를 결정합니다.
- 인덱스를 참조하여 읽어야 할 바이트 범위를 구합니다.
- 해당 범위를 Range GET으로 가져옵니다.
- GPU의 하드웨어 디코더로 디코딩합니다.
인덱스에는 원본 파일의 ETag도 포함되어 있습니다. Range GET 시에 대조하기 때문에, 인덱스를 만든 후에 파일이 교체되었다면 즉시 알 수 있습니다.
3. 대역폭과 레이턴시 측정
S3에서 직접 읽는 구성이라서 가장 먼저 궁금한 것은 '제시간에 가능한가'입니다. 그래서 스토리지 단독의 읽기 성능을 측정했습니다. 비교 대상은 S3, Lustre, 로컬 NVMe 세 가지였습니다.
측정에는 GPU 노드(네트워크 대역폭 200 Gbps)의 CPU만 사용했습니다. 같은 형식의 데이터를 세 스토리지에 두고 무작위 위치에서 읽었습니다.
측정 시 주의한 점은 캐시의 영향을 피하는 것이었습니다. 같은 장소를 반복해서 읽으면 어느 스토리지든 실제보다 빠르게 보입니다. 그래서 S3에는 약 21 TB, Lustre에는 약 3 TB의 데이터를 준비하여 같은 장소를 거의 다시 읽지 않도록 했습니다. Lustre와 NVMe는 OS 캐시를 거치지 않고 읽었습니다.
대역폭
먼저, 1초당 읽을 수 있는 데이터 양을 측정했습니다.
| 노드 수 | S3 | Lustre | 로컬 NVMe |
|---|---|---|---|
| 1 | 23.9 GB/s | 5.5 GB/s | 51.9 GB/s |
| ... | |||
| 로컬 NVMe의 4 노드 이상은, 1 노드의 실측값에 노드 수를 곱한 값입니다. |

노드 수와 읽기 대역폭의 합계. 로컬 NVMe의 점선은 1 노드의 값 × 노드 수
S3는 1 노드에서 약 24 GB/s, 20 노드에서 약 483 GB/s였습니다. 노드 수에 거의 비례하여 늘어났습니다.
단일 노드의 상한을 결정한 것은 S3가 아니라 노드의 네트워크였습니다. 어느 노드도 네트워크 수신량이 상한인 200 Gbps에 도달했습니다. CPU에는 아직 여유가 있었습니다. 20 노드에서는 1초당 약 6만 건의 요청을 보냈지만, S3 측에서 제한을 받는 일은 없었습니다.
Lustre는 4 노드에서 약 22 GB/s에 도달한 후 늘어나지 않았습니다. 12 노드로 해도 합계는 변함이 없고, 1 노드당 대역폭이 줄어듭니다. 공유 파일 시스템이기 때문에 모든 노드가 하나의 상한을 나누어 쓰는 형태가 됩니다.
Lustre의 값은 파일 시스템 계약 내용에 따라 달라집니다. FSx for Lustre는 확보한 용량에 비례하여 스루풋(throughput)이 결정되는 구조입니다. 용량을 늘리거나 더 높은 플랜을 선택하면, Lustre의 대역폭은 이보다 높아집니다. 다만, 그만큼 비용이 발생합니다.
로컬 NVMe는 1 노드에서 약 52 GB/s로 가장 빨랐고, 노드별로 독립적입니다. 하지만 용량에 한계가 있고 스테이징(staging)이 필요합니다.
실제 학습에 필요한 대역폭은 이 상한보다 훨씬 작은 값입니다. 대역폭이 부족할 걱정은 없었습니다.
CPU 사용량
S3의 대역폭에는 대가가 따릅니다. 로컬 NVMe에서는 데이터가 장치에서 메모리로 직접 쓰여지기 때문에, CPU는 거의 개입하지 않습니다. S3로부터의 읽기는 HTTPS 통신이므로, 패킷 수신과 TLS 복호화(decryption)를 CPU가 수행합니다.
노드 전체의 CPU 사용량을 측정해 보니, 1 GB를 읽는 데 필요한 CPU 시간은 NVMe가 약 0.05초, Lustre가 약 1초, S3가 약 1.5초였습니다. S3의 경우 이 중 3~4할은 TLS 복호화(decryption)에 사용되고, 나머지 대부분은 커널 네트워크 처리입니다. 대역폭을 NIC의 상한까지 사용할 경우, 노드 CPU의 약 3분의 1 정도가 읽기 작업만으로 채워집니다.
그렇다고는 해도, 학습이 실제로 읽는 양은 최대치보다 훨씬 작기 때문에, 읽기에 사용되는 CPU도 그만큼 줄어듭니다. 따라서 데이터 로더(data loader)의 프로세스 수를 결정할 때 이 부분을 고려해야 합니다.
레이턴시 (Latency)
다음으로, 한 번의 읽기에 걸리는 시간을 측정했습니다. 요청을 보낸 시점부터 지정된 범위의 모든 데이터가 도착할 때까지의 시간입니다. 본문에서는 이를 레이턴시라고 부릅니다. 3 MiB를 읽었을 때의 값입니다.
| 중앙값 (p50) | 100회 중 한 번 느린 경우 (p99) |
|---|---|
| 로컬 NVMe | 약 2 ms |
| ... | |
![]() |
레이턴시 분포(왼쪽)와 노드 수를 늘렸을 때의 변화(오른쪽). NVMe는 노드 로컬이므로 1노드의 값을 그대로 확장했습니다.
S3의 중앙값은 Lustre의 약 20배, NVMe의 약 70배였습니다. 실제 동영상 파일에 Range GET을 수행했을 때도 비슷한 수준의 값이었습니다.
S3의 특징은 읽는 양을 줄여도 빨라지지 않는다는 것입니다. 64 KiB든 4 MiB든 중앙값은 100ms 전후였습니다. 요청을 보낸 후 데이터가 도착하기 시작할 때까지의 대기 시간이 대부분을 차지하고 있습니다.
반면, S3의 레이턴시는 노드 수를 늘려도 변하지 않았습니다. S3는 1노드든 20노드든 중앙값이 125140ms, p99가 290360ms 범위에 머물렀습니다. Lustre는 반대로 노드를 늘릴수록 느린 읽기가 증가했습니다. p99는 1노드에서 약 110ms였으나, 12노드에서는 약 360ms로 S3와 같은 수준이 되었습니다. 중앙값은 Lustre가 훨씬 짧게 유지되었지만, 느린 읽기에 한정하면 노드 수가 많을 때의 차이는 사라집니다.
학습에 미치는 영향
140ms라는 값만 보면 치명적으로 보이지 않을 수 있습니다. 하지만 학습에서는 다음 두 가지 이유로 느린 읽기의 영향이 커집니다.
첫째, 1개의 샘플에 여러 번의 읽기가 필요합니다. 7대의 카메라 영상이 모두 모일 때까지 해당 샘플은 사용할 수 없습니다. 대기 시간은 7개 중 가장 느린 1개에 의해 결정됩니다.
둘째, 여러 GPU로 학습하고 있습니다. 각 스텝마다 모든 GPU의 계산이 완료되기를 기다립니다. 하나의 GPU 데이터가 지연되면 전체가 기다려지게 됩니다.
읽기 횟수가 늘어날수록, 어느 하나라도 느릴 확률은 높아집니다. 개별 측정 데이터로부터 N번을 동시에 읽었을 때의 대기 시간을 통계적으로 추정하면, 중앙값은 1개일 때 약 130ms, 7개일 때 약 230ms, 64개일 때는 약 330ms가 됩니다. 64개의 값은 1개의 p99와 거의 같습니다. 즉, '100회 중 한 번 느린 경우'가 매번의 대기 시간이 되는 것입니다.
결국 S3에서 직접 읽을 때 해결해야 할 문제는 대역폭이 아니라 레이턴시였습니다.
4. 선행 읽기를 통한 레이턴시 대책 (Prefetching)
선행 읽기가 없는 경우
초기 구현에서는 배치(batch)가 디코딩 직전까지 도착한 후에 Range GET을 요청했습니다. 이 방식으로는 S3의 느린 읽기가 그대로 학습의 대기 시간으로 이어집니다. GPU는 계산할 준비가 되어 있는데 데이터를 기다리며 멈추게 됩니다.
원리 (Mechanism)
S3의 레이턴시 자체를 바꿀 수는 없습니다. 그래서 읽기를 학습보다 미리 완료해 두기로 했습니다.
학습에 사용되는 배치의 순서는 샘플러(sampler)가 결정합니다. 즉, 앞으로 어떤 배치를 사용할지는 사전에 알 수 있습니다. 따라서 미래 배치(next batch)의 Range GET을 먼저 수행하고 그 결과를 CPU 메모리에 저장해 둡니다.

선행 읽기 흐름
디코딩이 필요한 배치에 도달했을 때, 필요한 데이터는 이미 메모리에 있습니다. 만약 어떤 읽기가 1초가 걸렸더라도, 선행 읽기로 확보한 여유 시간 내에서 끝나면 학습은 기다려지지 않습니다.
저장하는 것은 압축된 동영상 데이터이므로 크기가 크지 않습니다. 호스트(host)의 메모리를 사용하며 GPU 메모리는 사용하지 않습니다.
개선한 점
선행 읽기(Prefetching)의 개념 자체는 단순하지만, 설계 단계에서는 다음 3가지 포인트를 중요하게 다루었습니다.
1. 선행 읽기가 학습을 방해하지 않는다. 선행 읽기는 어디까지나 준비 과정입니다. 핵심 학습 흐름과는 분리하여 작동시키기 때문에, 선행 읽기가 지연되거나 실패하더라도 학습은 그대로 진행됩니다. 그 경우, 선행 읽기가 없을 때와 마찬가지로 필요할 때 S3에서 데이터를 읽어옵니다.
2. 효과를 숫자로 확인할 수 있게 한다. 선행 읽기는 효과가 없어도 학습이 정상적으로 진행됩니다. 이 때문에 '효과가 없다'는 사실을 인지하기 어려운 문제가 있습니다. 그래서 데이터가 대기한 시간이나, 선행 읽기가 유용했던 비율을 학습 통계로 산출하고 있습니다.
3. 선행 읽기의 깊이는 실측으로 결정한다. 너무 깊으면 메모리를 불필요하게 사용하고, 너무 얕으면 느린 읽기 속도를 숨길 수 없습니다. 그래서 깊이를 변경하며 데이터 대기 시간을 측정했고, 대기가 거의 0이 되는 최소의 깊이를 선택했습니다.
효과
선행 읽기를 적용한 결과, 디코더가 S3를 기다리는 시간이 99% 이상 감소했으며, GPU가 데이터를 기다리며 멈추는 현상이 거의 없어졌습니다.
다만, 이 효과를 얻기 위해서는 Python의 가비지 컬렉션(Garbage Collection)으로 인한 일시 정지 시간도 함께 줄여야 했습니다.
5. 운영 중 어려웠던 점
S3에서 대량의 데이터를 읽어오게 되면서 스토리지 외적인 부분에서 몇 번의 병목 현상을 겪었습니다. 같은 구성을 시도하는 분들의 참고가 되었으면 합니다.
DNS 조회(Query)가 너무 많다. S3에 연결할 때마다 이름 해석(Name Resolution)이 발생합니다. 연결 수가 늘어나면서 VPC의 DNS 제한에 걸려 이름 해석에 실패하게 되었습니다. 이 문제를 해결하기 위해 프로세스 내에서 해결 결과를 단기간 캐싱하도록 했습니다. 이번 측정에서도 20 노드가 동시에 읽을 때 이러한 실패가 나타났습니다.
인증 정보(Credential) 획득이 집중된다. 학습을 시작하면 다수의 프로세스가 한꺼번에 인증 정보를 가져가려고 합니다. 이것이 제한에 걸려 읽기 속도가 느려졌습니다. 이 문제를 해결하기 위해 인증 정보를 일괄적으로 가져오는 방식으로 변경했습니다.
Python 클라이언트는 1개 프로세스에서 성능 상한선(Headroom)에 도달한다. boto3는 스레드를 늘려도 개별 프로세스의 대역폭이 증가하지 않습니다. 이는 Python의 GIL(Global Interpreter Lock) 제약 때문입니다. 따라서 프로세스를 분리하거나, kvikio 또는 aws-crt처럼 네이티브로 구현된 클라이언트를 사용해야 합니다. 이번 측정에서는 kvikio (NVIDIA의 RAPIDS 프로젝트 라이브러리로 내부적으로 libcurl 사용)와 aws-crt (AWS Common Runtime) 모두 1 노드의 상한선까지는 도달했지만, boto3는 그 약 4분의 1 수준에서 성능이 정체되었습니다.
병렬도를 너무 높이면 느려진다. 동시에 요청하는 수를 늘리면 대역폭은 증가하지만, 어느 지점을 넘어서면 오히려 감소합니다.
측정 환경에서는 같은 위치를 반복해서 읽지 않는다. S3는 동일한 범위의 데이터를 짧은 간격으로 여러 번 읽으면 응답 속도가 몇 배로 빨라집니다. 측정용 데이터가 작을 경우, 이 효과 때문에 실제보다 빠르게 보일 수 있습니다. 처음에는 이를 인지하지 못하여 지연 시간을 3분의 1 수준으로 추정했습니다. 하지만 학습은 동일한 데이터를 계속 읽지 않으므로, 측정 시에도 읽을 때마다 다른 위치를 읽도록 해야 합니다.
6. 도입 후 변화
도입 전후 비교
| 이전 | 현재 |
|---|---|
| 학습에 사용할 수 있는 데이터 양 | 클러스터 스토리지에 들어가는 만큼 |
| ... | |
| 가장 큰 변화는 학습 데이터를 늘릴 때, 클러스터의 스토리지 용량을 고려할 필요가 없어졌다는 점입니다. 이전 구성에서는 스토리지에 담길 수 있는 양이 학습 데이터량의 상한선이었습니다. 현재는 S3에 있는 데이터를 그대로 학습에 사용할 수 있습니다. 앞으로 데이터가 더 많이 늘어나도, 스토리지가 이유로 인해 학습을 할 수 없게 되는 일은 없습니다. |
스테이징(Staging) 과정도 필요 없어졌습니다. 데이터셋이 S3에 있다면 GPU를 실행하고 바로 학습을 시작할 수 있습니다.
적합한 경우와 그렇지 않은 경우
S3에서 직접 읽어오는 방식이 모든 경우에 적합한 것은 아닙니다.
적합한 경우
- 데이터셋의 크기가 클러스터 스토리지에 담기지 않을 정도로 큰 경우.
- 다수의 노드에서 학습하는 경우. S3의 대역폭은 노드 수에 비례하여 증가하며, 지연 시간(Latency)은 노드 수에 따라 변하지 않습니다.
- 앞으로 어떤 데이터를 읽을지 미리 아는 경우. 선행 읽기(Prefetching)를 사용할 수 있습니다.
적합하지 않은 경우
• 작은 데이터를 하나씩 순차적으로 읽을 때. 한 번에 100ms 이상이 걸리므로, 선행 읽기(Prefetching)나 병렬화가 불가능하면 느려집니다.
• 응답 속도가 필요하고, 선행 읽기도 할 수 없을 때.
• 데이터 읽기에 CPU를 사용하고 싶지 않을 때. 3장에서 보았듯이, 1GB당 CPU 사용량은 Lustre의 약 1.5배, NVMe의 약 30배입니다.
로컬 NVMe는 이번에 측정된 것 중 가장 빠른 스토리지입니다. 데이터셋이 수용되고 스테이징 시간이 허용된다면, 여전히 유력한 선택지입니다.
결론
S3에서 직접 데이터를 읽어 학습함으로써, 학습에 사용할 수 있는 데이터의 양을 클러스터의 스토리지 용량으로부터 분리할 수 있었습니다. 하지만 이것은 시작일 뿐입니다. 대규모 데이터를 고속으로 학습하려면, 학습 기반 시설(Training Infrastructure)을 더욱 발전시켜야 합니다.
더 자세한 내용은 Techtalk에서 이야기할 예정이니, 관심 있는 분들은 꼭 확인해 주세요.
Turing에서는 대규모 학습 기반 시설을 개선하고 운영할 수 있는 경험 많은 소프트웨어 개발자를 모집하고 있습니다. 관심 있으신 분들은 채용 페이지를 통해 지원해 주십시오. 캐주얼 면담이나 사무실 견학 등 가벼운 교류도 환영합니다.
토론

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn ML의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기