Colab 무료 GPU 세션 끊김: 학습이 처음부터 도는 원인과 체크포인트 재개법
무료 Colab에서 학습을 한 시간쯤 돌리다 런타임이 사라지면, 다시 연결한 노트북은 데이터 다운로드부터 첫 에폭까지 전부 새로 시작합니다. /content 아래에 받아 둔 데이터와 학습 중이던 가중치가 VM과 함께 지워지기 때문입니다. 세션 끊김을 겪어 본 분이 가장 아까워하는 것은 GPU 시간보다 그렇게 날아간 진행 상황일 겁니다.
그래서 무료 플랜이 배정한 T4 런타임에서 CIFAR-10 ResNet-18 학습을 40에폭, 7,800스텝으로 걸어 두고 중간에 런타임을 일부러 지웠습니다. 그다음 Google Drive에 남긴 체크포인트로 이어 붙였습니다. 목표는 두 가지였습니다. 끊긴 지점부터 끊기지 않았을 때와 같은 숫자로 이어 가는 것, 그리고 새 런타임마다 데이터를 다시 받지 않는 것입니다.
막상 해 보니 체크포인트 코드 자체보다 그 주변에서 더 오래 막혔습니다. 지운 줄 알았던 런타임이 Drive에 계속 쓰면서 체크포인트를 밀어냈고, Hugging Face에서 받은 CIFAR-10은 행 순서가 원본과 달랐으며, torch 2.11의 torch.load 기본값은 제가 저장한 파일을 거부했습니다. 세 가지가 실제 출력에 어떻게 나타났는지, 각각 어떻게 풀었는지를 시간 순서대로 따라가 보겠습니다.
노트북은 VM 시계를 따라 UTC로 로그를 찍습니다. 본문에서는 9시간을 더한 한국 시간(KST)으로 바꿔 쓰고, 코드 블록 안의 출력은 화면에 찍힌 그대로 둡니다.
무료 T4에 학습을 걸고 일부러 런타임을 지웠습니다
실험 환경은 무료 플랜이 그날 내준 런타임 그대로입니다. 셀 1의 nvidia-smi가 보여 준 장치와 버전을 먼저 표로 옮겨 둡니다. 같은 노트북을 다른 날 열면 GPU 종류나 torch 버전이 바뀔 수 있어서, 결과를 읽을 때 기준점이 됩니다.
| 항목 | 값 |
|---|---|
| 플랜 | Colab 무료 |
| GPU | Tesla T4, 15360MiB |
| 드라이버 / CUDA | 580.82.07 / 13.0 |
| PyTorch | 2.11.0+cu130 (런타임 기본 설치) |
| 모델·데이터 | ResNet-18(첫 층 3×3, maxpool 제거), CIFAR-10 train 50000 / test 10000 |
| 학습 설정 | batch 256, 40에폭(에폭당 195스텝, 총 7800스텝), 혼합 정밀도 |
| 체크포인트 | 100스텝과 에폭 끝마다 Drive에 저장, 최근 3개 보관, 파일 하나 89.5MB |

T4가 잡힌 것을 보고 학습 셀을 실행했습니다. 그런데 첫 장애물은 GPU가 아니라 데이터였습니다. torchvision의 CIFAR10(download=True)는 cs.toronto.edu에서 170MB짜리 압축 파일을 받는데, 이날은 이 단계가 끝나지 않았습니다.
2026-10-17T07:31:58+00:00
[2026-10-17T07:32:00+00:00] device=Tesla T4 torch=2.11.0+cu130 cuda=13.0 model=resnet18 ckpt_dir=/content/drive/MyDrive/ainewsnote_colab_ckpt/run_20261017
100% 170M/170M [57:05<00:00, 49.8kB/s]
[2026-10-17T08:29:17+00:00] data train=50000 test=10000
[2026-10-17T08:29:17+00:00] no checkpoint found -> start from step 0
[2026-10-17T08:29:34+00:00] EPOCH 0 done: steps=195 time=17.7s loss=1.6886 test_acc=0.4907 gpu_mem_max=1113MB
다운로드에 57분 5초, 평균 49.8kB/s가 걸렸습니다. 16시 31분에 시작한 셀이 17시 29분에야 첫 스텝을 돌렸고, 그 뒤 첫 에폭은 17.7초, 이후 에폭은 대부분 15~16초였습니다. 40에폭을 다 합쳐도 다운로드 한 번보다 짧습니다. 이 한 시간은 /content가 비어 있는 새 런타임마다 다시 치러야 하는 비용이라, 끊김 대비를 체크포인트만으로 끝낼 수 없다는 점이 여기서 드러났습니다.

체크포인트 저장 쪽은 이렇게 짰습니다. 가중치만 저장하면 재개 후 학습률 스케줄과 옵티마이저 모멘텀이 처음 상태로 돌아갑니다. 그래서 옵티마이저·스케줄러·GradScaler 상태, 난수 상태, 몇 번째 배치까지 돌았는지를 한 파일에 담았습니다. 파일은 임시 이름으로 쓴 뒤 os.replace로 바꿉니다. 쓰는 도중에 런타임이 끊겨도 반쯤 쓴 파일이 정식 이름을 갖지 않게 하려는 장치입니다.
def save_ckpt(state, step):
os.makedirs(A.ckpt_dir, exist_ok=True); t0 = time.time()
path = os.path.join(A.ckpt_dir, f'ckpt_step{step:07d}.pt'); tmp = path + '.tmp'
torch.save(state, tmp); os.replace(tmp, path) # atomic: a half-written file never gets the real name
for old in sorted(glob.glob(os.path.join(A.ckpt_dir, 'ckpt_step*.pt')))[:-A.keep]: os.remove(old)
log(f'checkpoint saved step={step} file={os.path.basename(path)} size={os.path.getsize(path)/1e6:.1f}MB took={time.time()-t0:.2f}s')
# training loop
if step % A.save_every == 0 or bi == spe - 1:
nb, ne = (bi + 1, ep) if bi + 1 < spe else (0, ep + 1)
save_ckpt({'model': model.state_dict(), 'optimizer': opt.state_dict(), 'scheduler': sched.state_dict(),
'scaler': scaler.state_dict() if scaler else None, 'epoch': ne, 'batch_idx': nb, 'step': step,
'last_loss': loss_v, 'saved_at': now(), 'args': vars(A),
'rng': {'torch': torch.get_rng_state(), 'numpy': np.random.get_state(), 'python': random.getstate(),
'cuda': torch.cuda.get_rng_state_all() if amp else None}}, step)
로그로 보면 89.5MB 체크포인트를 Drive에 쓰는 데 0.2~0.8초가 들었습니다. 에폭이 15초 남짓이니 100스텝마다 저장해도 학습 속도에 거의 티가 나지 않습니다. 함수 안의 [:-A.keep]은 가장 최근 3개만 남기고 나머지를 지우는 로테이션입니다. Drive 용량을 아끼려고 넣은 이 한 줄이 뒤에서 예상하지 못한 장면을 만듭니다.
학습이 에폭 번호 23에 들어선 17시 35분, 런타임 메뉴의 연결 해제 및 런타임 삭제로 VM을 지웠습니다. 화면에 남은 마지막 줄은 17:35:28의 checkpoint saved step=4600과 17:35:32의 step=4650/7800이었고, 툴바는 Reconnect T4로 바뀌었습니다.

화면만 믿으면 학습은 4650에서 멈췄고, 다음 런타임은 4600에서 이어 가야 합니다. Drive는 다른 이야기를 하고 있었습니다.
지운 런타임이 Drive에 계속 쓰고 있었습니다: 체크포인트 로테이션의 함정
1분쯤 뒤 새 런타임을 연결해 같은 노트북을 처음부터 실행했습니다. 셀 4는 학습 셀보다 먼저 Drive의 체크포인트 폴더를 나열하고 steps.csv의 마지막 세 줄을 보여 줍니다.
2026-10-17T08:36:34+00:00
total 262231
-rw------- 1 root root 74 2026-10-17_08:29:17 attempts.csv
-rw------- 1 root root 89506827 2026-10-17_08:36:16 ckpt_step0005200.pt
-rw------- 1 root root 89506827 2026-10-17_08:36:21 ckpt_step0005265.pt
-rw------- 1 root root 89506827 2026-10-17_08:36:25 ckpt_step0005300.pt
-rw------- 1 root root 1047 2026-10-17_08:36:28 epochs.csv
no steps.csv yet
17:36:34에 본 폴더에는 4600이 아니라 5200, 5265, 5300이 있었고 수정 시각은 17:36:16~17:36:25였습니다. 런타임이 화면에서 사라진 뒤에도 50초 가까이 체크포인트가 새로 쓰였다는 뜻입니다. 5265는 에폭 27개가 끝난 지점(27×195)에서 저장된 파일입니다. 이상한 점은 하나 더 있었습니다. 학습 내내 5스텝마다 줄을 붙이던 steps.csv가 목록에서 아예 빠져 있었습니다.

이 런타임은 데이터부터 다시 받기 시작했는데 속도가 41.6kB/s였습니다. 한 시간을 각오하고 두었더니 진행률이 65%(110M/170M, 경과 44:59)에서 더 움직이지 않았고, 툴바는 다시 Reconnect T4가 되어 있었습니다. 노트북 제목 옆에는 Save failed가, 화면 아래에는 Automatic saving failed. This file was updated remotely or in another tab. 알림이 떠 있었습니다.

이 런타임이 왜 끊겼는지는 화면 어디에도 나오지 않습니다. 알림 문구는 같은 노트북 파일이 다른 곳에서 바뀌었다는 뜻이라, 앞서 지운 세션의 탭과 새 세션이 한 파일을 두고 저장을 겨룬 것으로 보고 있습니다. 어디까지나 추정입니다. 다음 시도에서는 기존 노트북 탭을 모두 닫고, 고친 노트북을 새 파일로 올려 열었습니다.
한 시간 반쯤 지나 새 노트북에서 Drive를 다시 열자 폴더 모습이 또 바뀌어 있었습니다.
2026-10-17T10:10:53+00:00
total 262537
-rw------- 1 root root 74 2026-10-17_08:29:17 attempts.csv
-rw------- 1 root root 89506827 2026-10-17_08:36:48 ckpt_step0005600.pt
-rw------- 1 root root 89506827 2026-10-17_08:36:52 ckpt_step0005655.pt
-rw------- 1 root root 89506827 2026-10-17_08:36:57 ckpt_step0005700.pt
-rw------- 1 root root 1121 2026-10-17_08:37:00 epochs.csv
-rw------- 1 root root 313501 2026-10-17_08:29:20 steps.csv
1,5753,29,0.125427,0.030826,2026-10-17T08:37:00+00:00
1,5754,29,0.146040,0.030800,2026-10-17T08:37:00+00:00
1,5755,29,0.113468,0.030773,2026-10-17T08:37:00+00:00
--- data_cache:
total 0
newest checkpoint: ckpt_step0005700.pt -> after 30 s: ckpt_step0005700.pt
stable: no other runtime is writing; cell 5 will resume from ckpt_step0005700.pt
5200~5300은 사라지고 5600, 5655, 5700이 남았습니다. 사라졌던 steps.csv도 313,501바이트로 돌아왔는데, 마지막 세 줄의 첫 칸이 시도 번호 1입니다. 처음 런타임이 쓴 기록이라는 뜻입니다. 마지막 스텝은 5755, 기록 시각은 17:37:00이었습니다. 숫자를 이어 보면, 학습은 화면에 찍힌 마지막 로그(17:35:32, step 4650)에서 멈추지 않고 17:37:00의 step 5755까지 88초를 더 갔습니다. 툴바가 Reconnect T4로 바뀐 화면을 캡처한 17:35:48 뒤로도 1분 넘게 이어진 셈이고, 그동안 로테이션이 새 런타임이 봤던 5200~5300을 지웠습니다.
이 해석에는 추정이 섞여 있어 근거를 나눠 둡니다. Drive의 파일 시각과 CSV 줄은 화면에 찍힌 사실입니다. 그 기록을 쓴 주체가 지운 런타임이라는 판단은 두 가지에서 나옵니다. 시도 번호가 1이라는 점, 그리고 17:35:32의 step 4650부터 17:37:00의 step 5755까지 1105스텝을 88초에 돈 속도(초당 12.6스텝 안팎)가 에폭당 195스텝·15~16초와 맞는다는 계산입니다. 삭제를 누른 정확한 시각은 로그에 남지 않아, 삭제 뒤 몇 초를 더 돌았는지는 숫자로 말하지 않겠습니다. 17:36:34의 새 런타임에 steps.csv가 보이지 않은 것은 다른 VM이 쓰던 파일이 아직 Drive에 반영되지 않았기 때문이라고 보지만, 로그만으로는 단정하기 어렵습니다.
아찔했던 것은 재개 지점이 틀어질 뻔했다는 점입니다. 17:36:34의 런타임이 데이터를 금방 받았다면 5300에서 재개했을 테고, 같은 폴더에 두 런타임이 동시에 체크포인트를 쓰면서 서로의 파일을 로테이션으로 지웠을 겁니다.
재개 셀 앞에 30초 대기를 넣은 이유
그래서 셀 4 끝에 짧은 검사를 붙였습니다. 최신 체크포인트 이름을 읽고 30초 뒤 다시 읽어, 그사이 바뀌었으면 Run all을 거기서 멈춥니다. 이 학습은 100스텝마다 저장하고 100스텝에 7~8초가 걸리므로, 30초 동안 이름이 그대로라면 다른 런타임이 이 폴더에 쓰고 있지 않다고 봐도 됩니다.
import glob, time
newest = lambda: (sorted(glob.glob('/content/drive/MyDrive/ainewsnote_colab_ckpt/run_20261017/ckpt_step*.pt')) or ['none'])[-1].split('/')[-1]
a = newest(); time.sleep(30); b = newest()
print(f'newest checkpoint: {a} -> after 30 s: {b}')
if a != b: raise RuntimeError('checkpoints are still being written by another runtime: wait a few minutes, then Run all again')
print('stable: no other runtime is writing; cell 5 will resume from', b)

19:10:53에 실행한 셀 4는 30초 전후 모두 ckpt_step0005700.pt를 읽었고, 학습 셀이 5700에서 이어 가도록 넘어갔습니다. 저장 간격을 크게 잡은 학습이라면 대기 시간도 그 간격보다 길게 잡아야 같은 효과가 납니다.
데이터는 Drive에 한 번만: Hugging Face 사본의 행 순서 함정
57분짜리 다운로드를 런타임마다 반복할 수는 없어서, 고친 노트북은 처음 한 번만 데이터를 받아 Drive의 data_cache 폴더에 두고 그다음부터는 거기서 /content/data로 복사하게 바꿨습니다. 찾는 순서는 런타임 안에 이미 풀린 데이터, Drive 캐시, Hugging Face 다운로드, 마지막으로 torchvision 원래 주소입니다. 받는 곳은 Hugging Face의 uoft-cs/cifar10 데이터셋을 커밋 0b2714987fa4로 고정했습니다. 커밋을 고정하지 않으면 저장소가 갱신될 때 같은 코드가 다른 파일을 받아 오게 됩니다.
캐시에 넣고 꺼낼 때는 임시 이름(.tmp)으로 복사한 뒤 sha256을 맞춰 보고 나서야 정식 이름을 붙입니다. 복사 도중에 런타임이 끊겨도 반쪽 파일이 캐시로 쓰이지 않게 하려는 것으로, 체크포인트 저장과 같은 생각입니다.
여기서 함정을 하나 밟을 뻔했습니다. Hugging Face 사본은 이미지와 라벨 자체는 torchvision과 같지만, train 5만 행이 원본 data_batch 4, 3, 2, 5, 1 순서로 들어 있습니다. 이 학습은 에폭마다 시드로 섞은 순열에서 배치를 뽑기 때문에, 데이터 순서가 바뀌면 같은 step 번호에 전혀 다른 이미지가 들어갑니다. torchvision 데이터로 시작한 체크포인트를 그대로 이어 가면 재개는 되겠지만, 끊기기 전과 같은 학습이라고 말할 근거가 사라집니다. 파일 sha256은 받은 파일이 온전하다는 것만 알려 줄 뿐 이런 순서 차이는 잡지 못합니다.
HF_REPO, HF_REV = 'uoft-cs/cifar10', '0b2714987fa478483af9968de7c934580d0bb9a2' # pinned commit
HF_TRAIN_TO_TV = [(40000, 50000), (20000, 30000), (10000, 20000), (0, 10000), (30000, 40000)] # HF rows for data_batch_1..5
# from_parquet(): after decoding the parquet rows
if name == 'train': # HF stores data_batch_4,3,2,5,1; put it back in torchvision order
x = np.concatenate([x[a:b] for a, b in HF_TRAIN_TO_TV]); y = np.concatenate([y[a:b] for a, b in HF_TRAIN_TO_TV])
# get_data(): whatever the source, check the tensors before training
for n in ('train', 'test'): # whatever the source: must be byte-identical to torchvision's CIFAR10 tensors, same order
x, y = out[n]; h = tensor_sha(x, y)
if h != TENSOR_SHA256[n]: raise SystemExit(f'{n} tensors differ from torchvision CIFAR10 (sha256 {h}); refusing to train')
순서를 되돌린 다음에는 이미지 바이트와 라벨을 이어 붙인 sha256을 torchvision CIFAR10에서 미리 구해 둔 값과 비교하고, 다르면 학습을 시작하지 않습니다. 출처가 캐시든 Hugging Face든 torchvision이든, 학습에 들어가는 텐서는 순서까지 한 가지로 고정됩니다. 고친 노트북의 첫 실행에서는 이렇게 나왔습니다.
[2026-10-17T10:11:33+00:00] data cache empty -> download
[2026-10-17T10:11:34+00:00] DOWNLOAD done hf_0b2714987fa4_train.parquet.part 119.7 MB in 1.5s (79660 kB/s)
[2026-10-17T10:11:35+00:00] DOWNLOAD done hf_0b2714987fa4_test.parquet.part 23.9 MB in 1.0s (23626 kB/s)
[2026-10-17T10:11:45+00:00] local -> Drive cache: hf_0b2714987fa4_train.parquet 119.7 MB in 0.7s, sha256 OK (8428b53a88a1...)
[2026-10-17T10:11:46+00:00] local -> Drive cache: hf_0b2714987fa4_test.parquet 23.9 MB in 0.6s, sha256 OK (841389e6f2d6...)
[2026-10-17T10:11:47+00:00] DATA source=Hugging Face uoft-cs/cifar10@0b2714987fa4 | train=50000 test=10000 | tensor sha256 = torchvision OK | 19.5s
같은 날 같은 T4 런타임인데 Hugging Face 쪽은 train 119.7MB를 1.5초(79,660kB/s)에 받았습니다. 57분 걸리던 단계가 PNG 디코딩과 해시 검사까지 합쳐 19.5초로 줄었고, 마지막 줄의 tensor sha256 = torchvision OK가 순서까지 원본과 같다는 것을 보여 줍니다. 받은 두 파일은 바로 Drive 캐시로 복사됐습니다.
torch 2.11에서 내 체크포인트가 UnpicklingError로 거부될 때
데이터가 준비되자 이번에는 체크포인트 로드에서 막혔습니다. 저장할 때와 불러올 때의 torch가 똑같이 2.11.0인데도 아무 옵션 없이 부른 torch.load(path)가 실패했습니다.
[2026-10-17T10:11:47+00:00] torch 2.11.0+cu130: torch.load(ckpt_step0005700.pt) with the default weights_only
[2026-10-17T10:11:52+00:00] torch.load failed: UnpicklingError | Weights only load failed. This file can still be loaded, to do so you have two options, do those steps only if you trust the source of the checkpoint.
[2026-10-17T10:11:52+00:00] (1) In PyTorch 2.6, we changed the default value of the `weights_only` argument in `torch.load` from `False` to `True`. Re-running `torch.load` with `weights_o
[2026-10-17T10:11:52+00:00] (2) Alternatively, to load with `weights_only=True` please check the recommended steps in the following error message.
[2026-10-17T10:11:52+00:00] WeightsUnpickler error: Unsupported global: GLOBAL numpy._core.multiarray._reconstruct was not an allowed global by default. Please use `torch.serialization.ad
[2026-10-17T10:11:52+00:00] Check the documentation of torch.load to learn more about types accepted by default with weights_only https://pytorch.org/docs/stable/generated/torch.load.html.
원인은 메시지 안에 다 있습니다. PyTorch 2.6에서 weights_only 기본값이 False에서 True로 바뀌었고, True일 때는 텐서와 기본 자료형, 허용 목록에 올린 타입만 풀어 줍니다. 제 체크포인트에는 재개 후에도 같은 증강과 셔플을 재현하려고 np.random.get_state()를 넣었는데, 이 값 안의 numpy 배열을 복원하는 _reconstruct가 허용 목록에 없어서 거부됐습니다. 모델 가중치만 담은 파일이었다면 겪지 않았을 오류이고, 재개를 제대로 하려고 상태를 많이 담을수록 걸리기 쉬운 오류입니다.
가장 쉬운 우회는 weights_only=False입니다. 하지만 이 옵션은 파일 안의 임의 객체를 그대로 되살리는 피클 로더를 열어 줍니다. 공유받은 체크포인트를 같은 코드로 불러오는 날이 오면 그대로 위험이 됩니다. 저는 막힌 타입 네 개만 safe_globals로 허용하고 weights_only=True를 유지했습니다.
def load_ckpt(path, device):
try:
return torch.load(path, map_location=device) # torch 2.6+ default: weights_only=True
except pickle.UnpicklingError as e:
log('torch.load failed:', type(e).__name__, '|', str(e).splitlines()[0][:160])
from numpy._core.multiarray import _reconstruct
allow = [_reconstruct, np.ndarray, np.dtype, type(np.dtype('uint32'))]
with torch.serialization.safe_globals(allow):
return torch.load(path, map_location=device, weights_only=True)
위 코드는 흐름이 보이도록 줄였습니다. 실제 노트북에는 numpy 1.x용 import 경로와, 허용 목록으로도 열리지 않을 때 이 스크립트가 직접 쓴 파일에 한해 weights_only=False로 여는 마지막 단계가 더 붙어 있습니다. 실행 결과는 이렇습니다.
[2026-10-17T10:11:52+00:00] loaded with weights_only=True + safe_globals: _reconstruct, ndarray, dtype, UInt32DType
[2026-10-17T10:11:52+00:00] RESUME from step=5700 epoch=29 batch=45 last_loss=0.0789 saved_at=2026-10-17T08:36:56+00:00 load_took=4.99s

네 타입을 허용하자 같은 파일이 weights_only=True로 열렸고, step 5700, 에폭 번호 29의 46번째 배치(0부터 세어 batch=45)부터 이어졌습니다. 89.5MB를 Drive에서 읽어 모든 상태를 되돌리는 데 4.99초가 걸렸습니다. 재개 지점이 5755가 아니라 5700인 것은 정상입니다. 5701~5755는 마지막 체크포인트 뒤에 돈 스텝이라 한 번 더 돌려야 합니다. 그 구간이 다음 절의 검증 재료가 됩니다.
끊기기 전에 돈 55스텝을 다시 돌려 loss를 맞춰 봤습니다
재개가 정말 이어진 것인지는 숫자로만 판단할 수 있습니다. steps.csv에는 시도 번호와 함께 스텝마다 loss를 남기므로, 두 런타임이 모두 돈 스텝을 골라 비교하면 됩니다. 같은 값이 나오려면 같은 스텝에 같은 배치와 같은 증강이 들어가야 해서, 증강 난수는 전역 스텝 번호로, 셔플 순열은 에폭 번호로 시드를 정하고 cuDNN을 결정적 모드로 고정했습니다.
torch.backends.cudnn.benchmark = False; torch.backends.cudnn.deterministic = True
def augment(xb, step): # random crop (pad 4) + flip, seeded by global step -> same batch after resume
g = torch.Generator().manual_seed(A.seed * 1_000_003 + step); B = xb.shape[0]
perm = torch.randperm(len(ytr), generator=torch.Generator().manual_seed(A.seed + ep)).to(dev)
재개한 학습은 에폭 29를 150스텝만 돌고(요약표에서 partial=yes) 나머지 에폭을 이어 갔습니다. 마지막 출력과 요약 셀의 결과는 다음과 같습니다.
[2026-10-17T10:14:47+00:00] EPOCH 39 done: steps=195 time=15.3s loss=0.0091 test_acc=0.9354 gpu_mem_max=1159MB
[2026-10-17T10:14:47+00:00] training finished at step=7800
[2026-10-17T10:14:53+00:00] steps run twice (after last checkpoint, before disconnect): 5701..5755 (55 steps), max |loss diff| = 0.000000


19:14:47에 7800스텝이 끝났고 마지막 에폭의 test_acc는 0.9354였습니다. 두 번 돈 5701~5755의 55스텝은 loss가 소수점 여섯째 자리까지 같았습니다. 같은 체크포인트, 같은 데이터 순서, 같은 시드라면 끊긴 구간을 다시 돌려도 결과가 갈라지지 않는다는 뜻입니다. 동시에 행 순서를 되돌린 Hugging Face 데이터가 torchvision 데이터와 같은 배치를 만들었다는 확인이기도 합니다. 순서 복원을 빼먹었다면 이 비교부터 성립하지 않았을 겁니다. GPU 최대 메모리는 처음 런타임이 1113MB, 재개 후가 1158~1159MB로 조금 달랐지만 이 55스텝의 loss에는 차이가 없었습니다.
새 런타임에서 Drive 캐시만으로 다시 붙였을 때
캐시가 실제로 다운로드를 대신하는지 보려고 새 런타임을 연결해 19:20:07에 Run all을 눌렀습니다. 출력이 Drive 캐시를 복사하는 줄로 시작하는 것은 런타임 안에 풀어 둔 데이터가 없는 새 VM이라는 표시입니다. 학습은 이미 7800에서 끝났으므로 이번 실행에서 볼 것은 데이터 준비와 로드뿐입니다.
[2026-10-17T10:20:24+00:00] cache -> local: hf_0b2714987fa4_train.parquet 119.7 MB in 10.7s, sha256 OK (8428b53a88a1...)
[2026-10-17T10:20:26+00:00] cache -> local: hf_0b2714987fa4_test.parquet 23.9 MB in 2.1s, sha256 OK (841389e6f2d6...)
[2026-10-17T10:20:37+00:00] DATA source=Drive cache hf_0b2714987fa4_*.parquet | train=50000 test=10000 | tensor sha256 = torchvision OK | 28.5s
[2026-10-17T10:20:41+00:00] RESUME from step=7800 epoch=40 batch=0 last_loss=0.0054 saved_at=2026-10-17T10:14:45+00:00 load_took=3.95s
[2026-10-17T10:20:42+00:00] training finished at step=7800

다운로드 줄은 하나도 없었고, 데이터는 Drive에서 복사해 해시까지 맞춘 뒤 28.5초 만에 준비됐습니다. 의외였던 것은 Drive에서 복사한 시간(train 10.7초)이 같은 날 Hugging Face에서 바로 받은 시간(1.5초)보다 길었다는 점입니다. 그러니 캐시의 쓸모는 속도가 아닙니다. 같은 날 torchvision 주소는 49.8kB/s, Hugging Face는 79,660kB/s로 천 배 넘게 차이가 났습니다. 외부 서버 속도는 내 쪽에서 정할 수 없지만, Drive 사본은 그 변동과 무관하게 해시가 맞는 같은 데이터를 30초 안쪽에 돌려줍니다.
공식 문서도 같은 방향을 가리킵니다. Colab FAQ는 무료 리소스가 보장되지도 무제한도 아니고 사용 한도가 수시로 바뀌며, VM은 한동안 유휴 상태면 삭제된다고 적고 있습니다. 런타임이 언제 사라질지는 사용자가 정할 수 없으니, 사라져도 잃지 않을 것을 런타임 밖에 두는 수밖에 없습니다.
참고한 문서는 Google Colab FAQ, PyTorch torch.load 문서(weights_only와 add_safe_globals 설명), Hugging Face uoft-cs/cifar10 데이터셋 카드, torchvision CIFAR10 문서입니다(확인일 2026-10-17).
무료 Colab에서 끊겨도 이어지는 학습, 제가 고른 구성
| 데이터 준비 방식 | 이날 측정값 | 새 런타임에서 | 제 판단 |
|---|---|---|---|
| torchvision 원래 주소(cs.toronto.edu) | 170MB에 57분 5초, 49.8kB/s | 매번 다시 받음 | 마지막 대체 경로로만 |
| Hugging Face 고정 커밋 직접 받기 | train 1.5초·test 1.0초, 준비 19.5초 | 매번 다시 받음, 행 순서 복원 필요 | 캐시가 비었을 때 첫 출처 |
| Drive 캐시에서 복사 | train 10.7초·test 2.1초, 준비 28.5초 | 다운로드 없음, 해시로 확인 | 기본 경로 |
표에서 가장 빠른 줄은 Hugging Face 직접 받기였지만, 저는 Drive 캐시를 기본으로 둡니다. 9초 남짓 늦는 대신 그날 외부 서버가 49.8kB/s로 떨어지든 말든 30초 안쪽에서 끝나기 때문입니다. 캐시가 비어 있을 때만 Hugging Face를 쓰고, torchvision 원래 주소는 둘 다 막혔을 때의 대체 경로로만 남깁니다.
체크포인트를 Drive에 두는 것만으로 끝났다고 보지 않습니다. 이번에는 런타임이 사라진 화면이 뜬 뒤에도 1분 넘게 학습이 이어졌고(steps.csv 마지막 줄 17:37:00, 기록 주체가 옛 런타임이라는 것은 추정), 그사이 로테이션이 새 런타임이 본 파일을 지웠습니다. 재개 셀 앞에는 최신 파일이 저장 간격보다 긴 시간 동안 그대로인지 보는 검사를 두고, 바뀌면 멈추게 하는 구성이 맞다고 봅니다.
torch 2.6 이후 버전에서 재개 코드를 쓴다면 체크포인트에 무엇을 넣었는지부터 봅니다. numpy 난수 상태처럼 텐서가 아닌 값이 들어 있으면 기본 torch.load가 거부하므로, weights_only=False로 문을 활짝 열기보다 막힌 타입만 safe_globals에 올리는 쪽을 고릅니다. 모델 가중치만으로 충분한 경우라면 애초에 그 상태를 빼는 것도 방법입니다.
데이터 출처를 바꿀 때는 파일 체크섬이 아니라 텐서 해시로 같은 데이터인지 판단합니다. Hugging Face 사본처럼 내용은 같고 순서만 다른 경우는 파일 해시로 드러나지 않고, 그대로 쓰면 같은 step에 다른 배치가 들어가 재개 전후를 비교할 근거가 사라집니다.
제 결론은 이렇습니다. 이 정도 학습에는 무료 T4로 모자람이 없었고, 문제는 언제든 사라질 수 있는 런타임 쪽이었습니다. 진행 상황과 데이터를 Drive에 두고, 재개 전에 다른 런타임이 아직 쓰고 있지 않은지 보고, 다시 돈 구간의 loss가 같은지 숫자로 맞춰 보는 데까지가 한 묶음입니다. 이번 기록에서 학습 도중 런타임을 일부러 지운 것은 17시 35분의 한 번입니다. 그 뒤 데이터를 다시 받던 런타임은 18시 38분 화면에서 이미 저절로 끊겨 있었고(진행 막대가 멈춘 경과 시간으로 보면 18시 21분 무렵), 학습 전이라 시도 기록에는 남지 않았습니다. 고친 노트북은 남아 있던 체크포인트 5700에서 이어 7800스텝, test_acc 0.9354로 학습을 끝냈고, 그 뒤 새로 연결한 런타임은 다운로드 없이 Drive 캐시와 7800 체크포인트만으로 준비를 마쳤습니다. 셋째 실행 뒤 요약 셀(19:20:48)이 보여 준 시도 기록도 세 줄입니다. 5755에서 멈춘 첫 시도, 5700에서 이어 7800까지 간 둘째, 7800에서 이어 더 돌 스텝이 없던 셋째입니다.





