본문 바로가기
C.W.K.
Lesson 02 of 06 · published

safetensors, 그리고 이것이 이긴 이유

~13 min · safetensors, pickle, security, format

Level 0Kindling
0 XP0/32 lessons0/10 achievements
0/100 XP to next level100 XP to go0% complete

이 포맷이 푼 문제는 결코 속도만이 아니었다

딥러닝 역사의 대부분 동안 "모델 파일"은 pickle을 의미했어: 파이썬의 무엇이든 직렬화하는 포맷으로, 객체에 대한 생성 절차를 기록하고 로드 시점에 재생하는 방식으로 동작하지. 그 일반성이 정확히 그 위험이야. pickle을 로드하는 건 로직을 실행하는 거야 — 임의의, 보이지 않는, 로드당 한 번의 로직. 악의적인 체크포인트는 가정이 아니야: pickle을 로드한다는 건 언제나 그 생성 로직을 실행한다는 뜻이었고, 정교하게 만든 pickle은 로드 시점에 코드를 실행해. 최근 PyTorch(2.6+)는 weights_only=True가 기본이라 임의 실행은 막아 — 무심한 이를 돕는 보호 기본값이지, 포맷 자체가 무엇인지를 바꾸진 않아. pickle 시대의 모든 "낯선 이에게서 웨이트 받기" 워크플로우는 친근한 얼굴을 한 신뢰 불가능 코드 실행이었던 거지.

safetensors는 정반대의 판을 걸었어: 행동이 아니라 데이터를 직렬화하는 것. 파일은 각 텐서를 기술하는 작은 JSON 헤더 — 이름, dtype, shape, 바이트 오프셋 — 뒤에 원시 텐서 바이트가 따라오는 구조야. 파일 안의 그 무엇도 실행 가능하지 않아. 로드는 JSON을 파싱하고 바이트를 메모리 매핑하는 일이야. 두 결과가 즉시 따라와: 로드 시점의 코드 실행이 없다는 것(보안 성질), 그리고 텐서 하나 안 읽고도 헤더만으로 레이아웃 전체를 안다는 것(검사 가능성). 더 빠른 zero-copy 로딩은 거의 부수효과였어 — 진짜 수확은 신뢰와 판독성이었지.

헤더 읽기는 기록관의 초능력이다

헤더가 파일 앞부분의 평범한 JSON이기 때문에, 표준 라이브러리만으로 safetensors 아티팩트를 감사할 수 있어: 파일을 열고, 8바이트 리틀 엔디언 헤더 길이를 읽고, 헤더 자체를 읽으면 돼. 텐서 인벤토리(이름, dtype, shape)를 전체 읽기를 저지르기 전에 알게 되는 거야. 아카이브에는 최소 세 군데서 이게 중요해: 착지한 것이 메타데이터가 약속한 것과 일치하는지 검증할 때(dtype 포렌식, 다음 레슨들), 같은 모델이라 주장하는 두 아티팩트를 diff할 때, 그리고 README 없이 낡은 드라이브에서 꺼낸 정체불명 파일을 분류할 때.

생태계는 기본값으로 투표했어: 주요 허브와 학습 프레임워크들이 배포용 표준 직렬화로 safetensors로 옮겼고, pickle 포맷은 코드와 체크포인트가 신뢰 도메인을 공유하는 내부 용도로 남겨 뒀지. 수집 관점에서는 safetensors를 기대되는 포맷 세대로 다루고 — 낯선 업로더의 pickle 포맷 아티팩트를 데이터 질문 이전의 보안 질문으로 다뤄.

pickle은 생성을 재생하고, safetensors는 데이터를 기술한다. 하나는 로드 시 실행되고 하나는 영원히 안 돼. 수 년 뒤에 파일을 로드할 계획인 아카이브는, 아무것도 실행할 수 없는 포맷을 향한 강한 편향을 가져야 해.

헤더에 손 대보기

아래 연습은 safetensors 헤더를 표준 라이브러리 파이썬 열두 줄로 읽어 — ML 프레임워크도 의존성도 없이. 이미 갖고 있는 파일과 갓 내려받은 파일에 대해 각각 돌려 봐. 그게 출력하는 인벤토리가 config 파일과 모델 카드가 약속만 할 수 있는 것들의 실측이야.

Code

의존성 제로로 safetensors 헤더 읽기·python
import json, struct

path = "model-00001-of-00003.safetensors"
with open(path, "rb") as f:
    n = struct.unpack("<Q", f.read(8))[0]      # 헤더 길이 (LE u64)
    header = json.loads(f.read(n))             # 평범한 JSON 텐서 맵

# 'metadata'는 포맷 메타데이터를 담을 수 있고; 나머지는 전부 텐서
meta = header.pop("__metadata__", {})
print("file metadata:", meta)
tensors = header
print(f"{len(tensors)} tensors")
for name, spec in sorted(tensors.items())[:10]:
    print(f"  {spec['dtype']:>8} {str(spec['shape']):<24} {name}")

# dtype 센서스 — 아티팩트 정밀도의 지문
census = {}
for spec in tensors.values():
    census[spec["dtype"]] = census.get(spec["dtype"], 0) + 1
print("dtype census:", census)
# 센서스가 F32인데 카드가 BF16이라면, 라벨이 절대
# 알려주지 않았을 무언가를 방금 안 거야.

External links

Exercise

지금 갖고 있는 safetensors 샤드 전부에 헤더 리더를 돌려 봐. 파일마다 기록해: 텐서 수, dtype 센서스, 아무 텐서 하나의 이름/shape. 그다음 센서스를 리포 config나 카드가 주장한 dtype과 비교해. 불일치가 있으면 그게 발견이야 — 어느 쪽을 믿을지, 왜인지 적어.
Hint
헤더 읽기는 싸(바이트지 텐서가 아니야)서 아카이브 전체에 돌리는 데 몇 초밖에 안 걸려. 쫓을 만한 불일치: config는 bfloat16인데 센서스는 F32(업캐스트?), 센서스에 dtype이 섞임(부분 변환된 아티팩트).

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.