728x90

주말에 Business Insider가 "Hugging Face가 인수 제안을 받고 있다"고 흘리더니, 며칠 뒤 Nvidia가 130억 달러 이상 밸류로 협상 중이라는 후속 기사가 나왔다. 아직 딜이 체결된 건 아니고 깨질 수도 있다는 단서가 붙어 있다. 그럼에도 HN 프론트에 하루 종일 올라와 있던 이유는 단순하다. 우리 대부분이 from_pretrained() 한 줄로 남의 회사 CDN에 프로덕션 빌드를 의존시켜 놓고 있기 때문이다.

왜 지금 이 뉴스가 인프라 이슈인가

기사에 나온 팩트만 정리하면 이렇다.

  • Nvidia가 Hugging Face 인수를 최근 몇 주간 논의했고, 밸류에이션은 130억 달러 이상.
  • 아직 합의된 딜은 없고 무산될 수 있다.
  • Nvidia는 이미 2023년 2.35억 달러 라운드(밸류 45억 달러)에 참여한 기존 투자자다.
  • 작년 말 Nvidia가 70억 달러 밸류로 5억 달러 투자를 제안했으나 Hugging Face가 거절했다. 이유는 "의사결정을 좌우할 지배적 투자자를 원치 않는다"였다.
  • Microsoft도 미팅을 했지만 현재 논의는 진행 중이 아니다.
  • 기사 자체가 지적하는 리스크: Hugging Face의 강점인 중립성. 이 플랫폼은 AMD, Intel 등 Nvidia 경쟁사 하드웨어도 지원한다.

여기서 실무자가 봐야 할 지점은 "누가 누굴 샀다"가 아니다. 우리 파이프라인에서 huggingface.co가 사실상 단일 장애점(SPOF)이자 단일 벤더 의존점이라는 사실이 다시 드러난 것이다. 나는 예전에 HF Hub 장애로 새벽에 배포가 통째로 밀린 적이 있다. 이미지 빌드 단계에서 토크나이저를 받아오게 해놨는데, Hub가 5xx를 뱉으니 CI가 전부 빨간불이 됐다. 그때 알았다. 우리는 모델을 "설치"하는 게 아니라 매 빌드마다 "다운로드"하고 있었다는 걸.

인수 여부와 무관하게, 이 뉴스는 그 의존성을 점검하라는 알림 정도로 받아들이면 된다.

핵심: Hugging Face는 라이브러리가 아니라 레지스트리다

Transformers를 pip으로 깔았다고 해서 의존성이 끝난 게 아니다. HF는 구조적으로 Docker Hub나 npm registry에 훨씬 가깝다. 코드는 로컬에 있지만, 가중치와 설정 파일은 런타임에 원격에서 끌어온다. 컨테이너 이미지를 만들었는데 FROM 베이스 이미지를 매번 인터넷에서 pull 하도록 방치한 것과 같은 상태다.

실제로 뭐가 어디서 오는지 확인해보자.

# 캐시가 실제로 어디에 얼마나 쌓여 있는지
$ pip install -U "huggingface_hub[cli]"
$ hf cache scan

REPO ID                     REPO TYPE SIZE ON DISK NB FILES LAST_MODIFIED
--------------------------- --------- ------------ -------- -------------
sentence-transformers/all-M model            90.9M        6 2 days ago
BAAI/bge-m3                 model             2.2G        9 3 weeks ago

Done in 0.0s. Scanned 2 repo(s) for a total of 2.3G.

(CLI 명령 이름은 버전에 따라 huggingface-cli scan-cache 형태였다가 최근 hf로 정리되는 흐름이다. 설치된 버전에서 hf --help로 확인하는 걸 권한다.)

중요한 건 이 캐시가 빌드 산출물이 아니라 런타임 사이드 이펙트라는 점이다. 컨테이너를 새로 띄우면 캐시는 비어 있고, 첫 요청 때 수 GB를 받는다. 스케일 아웃 시 파드 10개가 동시에 같은 2GB 모델을 받으면 egress 비용도 비용이지만 rate limit에 걸린다.

그래서 실무에서 해야 할 일은 명확하다. 모델을 아티팩트로 취급하고, 우리 스토리지에 고정(pin)해서 넣는 것. 컨테이너 이미지를 사내 레지스트리에 미러링하는 것과 완전히 같은 논리다.

# 1) 리비전을 커밋 해시로 고정해서 받는다 (main은 언제든 바뀐다)
$ hf download BAAI/bge-m3 \
    --revision 5617a9f61b028005a4858fdac845db406aefb181 \
    --local-dir ./models/bge-m3

# 2) 사내 S3(또는 MinIO)에 올린다
$ aws s3 sync ./models/bge-m3 s3://ml-artifacts/models/bge-m3/5617a9f6/ \
    --endpoint-url https://minio.internal

upload: models/bge-m3/config.json to s3://.../config.json
upload: models/bge-m3/model.safetensors to s3://.../model.safetensors

그리고 런타임에서는 네트워크를 아예 끊어버린다.

# 오프라인 강제. Hub를 쳐다보지도 않게 만든다.
export HF_HUB_OFFLINE=1
export HF_HOME=/opt/hf-cache

# 로컬 경로에서만 로드
python - <<'PY'
from transformers import AutoModel, AutoTokenizer
p = "/opt/models/bge-m3"
tok = AutoTokenizer.from_pretrained(p, local_files_only=True)
mdl = AutoModel.from_pretrained(p, local_files_only=True)
print("loaded:", mdl.config.model_type)
PY
# 출력: loaded: xlm-roberta

이렇게 해두면 인수가 되든 정책이 바뀌든 CDN이 죽든, 우리 서비스는 영향을 안 받는다.

실무 관점: 트레이드오프와 흔한 함정

중립성 리스크는 "당장"보다 "누적"으로 온다

기사가 짚은 대로 HF의 자산은 중립성이다. AMD ROCm, Intel Gaudi 계열 백엔드도 붙어 있고, Optimum 계열 통합도 여러 벤더가 걸려 있다. Nvidia가 인수한다고 해서 다음 날 AMD 지원이 삭제될 리는 없다. 그런 건 커뮤니티 반발만 부른다.

현실적으로 우려되는 건 더 미묘한 쪽이다. 기본값과 문서에서 밀어주는 경로, 최적화 리소스가 어디로 가느냐. 지금도 CUDA 경로가 가장 잘 굴러가는데, 그 격차가 유지보수 우선순위 차이로 더 벌어지는 시나리오. 다만 이건 어디까지나 가설이고, 딜 자체가 성사되지 않았다. 지금 아키텍처를 갈아엎을 근거는 없다. 대신 "우리가 CUDA 전용 코드에 얼마나 묶여 있나"를 한 번 세어보는 계기 정도는 된다.

흔한 함정 1: 오프라인 전환했는데 갑자기 터지는 케이스

HF_HUB_OFFLINE=1을 켜고 배포했더니 스테이징에서만 죽는 경우가 있다. 대표적으로 이 에러다.

OSError: We couldn't connect to 'https://huggingface.co' to load the files, and it looks like
/opt/models/bge-m3 is not the path to a directory containing a file named config.json.
Checkout your internet connection or see how to run the library in offline mode at
'https://huggingface.co/docs/transformers/installation#offline-mode'.

원인 90%는 hf download--local-dir을 안 줘서 캐시 디렉터리(~/.cache/huggingface)에만 들어갔고, 컨테이너에서 HF_HOME이 다른 경로를 보고 있는 것이다. 빌드 스테이지와 런타임 스테이지의 HF_HOME이 다르면 멀티스테이지 Dockerfile에서 특히 잘 터진다. 캐시를 COPY 할 거면 경로를 양쪽에서 동일하게 맞춰야 한다.

흔한 함정 2: rate limit / 인증

CI에서 병렬 잡을 돌리면 이걸 만난다.

huggingface_hub.errors.HfHubHTTPError: 429 Client Error: Too Many Requests for url:
https://huggingface.co/api/models/...

혹은 gated 모델(라이선스 동의 필요)일 때:

huggingface_hub.errors.GatedRepoError: 403 Client Error.
Cannot access gated repo for url https://huggingface.co/api/models/.../revision/main.
Access to model ... is restricted. You must have access to it and be authenticated to access it.

gated 모델은 계정 단위로 동의가 걸려 있어서, 개인 계정 토큰으로 CI를 돌리다가 그 사람이 퇴사하면 파이프라인이 죽는다. 실제로 겪었다. 조직 계정 + 조직 토큰으로 옮기고, 애초에 사내 미러에 한 번 받아두는 게 근본 대책이다.

흔한 함정 3: 리비전 미고정

from_pretrained("org/model")는 기본이 main이다. 업스트림이 가중치를 갱신하면 코드 변경 없이 추론 결과가 바뀐다. 임베딩 모델이면 벡터 DB에 저장된 기존 임베딩과 차원/분포가 어긋나서 검색 품질이 조용히 망가진다. 로그에 에러가 안 찍히니 발견도 늦다. revision=에 커밋 해시를 박아라. 이건 latest 태그 쓰지 말라는 얘기와 정확히 같다.

대안은 뭐가 있나

  • 사내 미러 + 오브젝트 스토리지: 가장 현실적. S3/MinIO에 넣고 initContainer로 내려받아 emptyDir이나 PVC에 꽂는다. 노드 로컬 디스크에 캐시하면 콜드 스타트도 줄어든다.
  • OCI 아티팩트로 패키징: 모델을 컨테이너 이미지나 ORAS 아티팩트로 만들어 사내 레지스트리에 올린다. 이미 있는 이미지 스캐닝·서명·권한 체계를 그대로 재활용할 수 있다는 게 크다.
  • MLflow / 자체 모델 레지스트리: 모델 버전 관리와 승격 워크플로가 필요하면. 다만 운영 부담이 늘어난다.
  • 추론 런타임 다변화: vLLM, llama.cpp, ONNX Runtime 등 가중치 포맷 표준(safetensors, GGUF, ONNX)에 기대는 구성이면 백엔드 교체 여지가 커진다. 반대로 특정 벤더 전용 최적화 포맷에 파이프라인을 통째로 맞춰놓으면 나중에 이사 비용이 크다.

트레이드오프는 분명하다. 미러링은 공짜가 아니다. 스토리지 비용, 동기화 잡 관리, "업스트림에 새 버전 나왔는데 우리 미러엔 없다"는 운영 부담이 생긴다. 모델을 한두 개만 쓰는 소규모 팀이면 굳이 안 해도 된다. 다만 프로덕션 크리티컬 경로에 있는 모델만큼은 반드시 고정하고 미러링하길 권한다. 개수로 치면 보통 3~5개면 충분하다.

정리

한 줄 요약: 인수는 아직 확정이 아니지만, huggingface.co를 런타임 의존성으로 두고 있다면 지금이 아티팩트로 승격시킬 타이밍이다.

누가 언제 해야 하나:

  • 지금 당장 — 프로덕션 추론 서비스에서 from_pretrained("org/model")을 리비전 없이 호출하는 팀. 인수설과 무관하게 이미 위험하다.
  • 이번 분기 안에 — CI/CD 빌드 단계에서 Hub를 때리는 팀. 외부 장애가 곧 배포 중단이다.
  • 관망해도 되는 쪽 — 연구/프로토타이핑 환경. 여기까지 통제하면 개발 속도만 죽는다.
  • 같이 점검 — AMD/Intel 하드웨어를 쓰거나 검토 중인 팀은, 딜 진행 상황과 각 백엔드의 릴리스 주기를 한동안 지켜볼 필요가 있다. 다만 지금 판단을 바꿀 근거는 아직 없다.

덧붙이면, 이런 뉴스가 나올 때마다 아키텍처를 뒤집는 건 좋은 습관이 아니다. 대신 "이 벤더가 내일 사라지거나 정책을 바꾸면 우리는 며칠 안에 복구 가능한가"를 문서 한 장으로 답할 수 있게 만들어두는 것. 그게 이번 뉴스에서 건질 실질적인 액션이다.

참고 자료

728x90
728x90

어느 날 아침 Hacker News 상단에 스크롤 애니메이션만 잔뜩 있는 랜딩 페이지 하나가 올라왔다. Discovery Loop. 제품도, 문서도, 가격표도 없다. 있는 건 "과학적 발견은 병목에 걸려 있다"는 문장과, 창업 멤버 네 명의 이름(Jeff Dean, Sanjay Ghemawat, Quoc Le, Oriol Vinyals), 그리고 채용 링크뿐이다.

MapReduce·GFS·BigTable·Spanner·TensorFlow·TPU를 만든 사람들이 "실험 루프를 자동화하겠다"고 하면, 그건 연구 선언문이 아니라 인프라 선언문으로 읽어야 한다. 이 글은 그 관점에서 쓴다. 우리가 이미 사내에서 굴리고 있는 실험 파이프라인과 뭐가 같고, 뭐가 다르고, 스케일을 올릴 때 실제로 어디서 터지는지.

1. 왜 지금 이게 화제인가

원문의 문제 정의는 간결하다. 과학적 방법론 자체는 훌륭한데, 실행 단계가 반복 노동이라는 것.

you propose an experiment, implement and run it, examine the results, then iterate to refine your approach.

실험 제안 → 구현/실행 → 결과 확인 → 개선. 이거 어디서 많이 본 루프다. 우리가 매일 하는 것이기도 하다. 코드 푸시 → CI 빌드 → 테스트 리포트 → 수정. 다른 점은 CI는 이미 자동화됐는데, ML 연구나 과학 실험의 루프는 아직 사람이 "다음에 뭘 해볼까"를 판단하고 손으로 잡을 던진다는 것.

Discovery Loop가 하겠다는 건 그 판단 단계까지 프론티어 모델에 맡기고, 대규모 컴퓨트로 수천 개 실험을 동시에 굴려 반복 주기를 압축하는 것이다. 첫 타깃도 명시돼 있다: ML 리서치/엔지니어링 자체를 먼저 자동화하고, 그걸로 자기네 스택을 최적화한다(act as our own first customer). 그다음이 NAE Grand Challenges — 신약, 헬스 인포매틱스, 태양광 경제성, 깨끗한 물, 사이버스페이스 보안.

참고로 사이트에는 투자 규모, 출시 일정, 벤치마크 수치 같은 건 일절 없다. 지금 시점에서 확인 가능한 건 "미션 + 팀 + 채용"이 전부고, 그 이상은 추측이다. 그러니 여기서는 기술 주장 대신 이 구조를 실제로 만들면 어디가 아픈지를 이야기하는 게 낫다.

2. 핵심 원리: 결국 상태 머신 + 스케줄러 + 평가 하네스

"AI가 실험을 자동으로 설계한다"는 말은 멋있지만, 구현체로 내려오면 놀랄 만큼 익숙한 세 조각이다.

  • 제안기(proposer): 다음에 시도할 설정을 만든다. 하이퍼파라미터든, 코드 패치든, 아키텍처든. 기존에는 베이지안 최적화나 사람이었고, 이제는 LLM이 들어간다.
  • 실행기(runner): 그 설정을 격리된 환경에서 돌린다. 컨테이너, GPU 할당, 타임아웃, 재시도. 딱 잡 스케줄러 영역.
  • 평가기(scorer): 결과를 기계가 읽을 수 있는 숫자로 만든다. 원문이 "learning loop with measurable outcomes"라고 못 박은 이유가 여기 있다. 측정이 안 되면 루프가 안 돈다.

가장 단순한 형태를 코드로 쓰면 이렇다. 실전 파이프라인의 골격도 사실 이것과 크게 다르지 않다(상태 저장을 sqlite로 두는 게 포인트 — 재시작이 공짜가 된다).

import json, sqlite3, subprocess

db = sqlite3.connect("loop.db")
db.execute("CREATE TABLE IF NOT EXISTS trial(id INTEGER PRIMARY KEY, cfg TEXT, score REAL)")

def propose(history):            # LLM이든 optuna든 여기만 갈아끼운다
    best = max(history, key=lambda h: h[1], default=({"lr": 1e-3}, 0))[0]
    return {"lr": best["lr"] * 0.5}

def run_and_score(cfg):
    out = subprocess.run(["python", "train.py", "--lr", str(cfg["lr"])],
                         capture_output=True, text=True, timeout=1800)
    return json.loads(out.stdout.strip().splitlines()[-1])["val_acc"]

hist = [(json.loads(c), s) for c, s in db.execute("SELECT cfg, score FROM trial")]
for _ in range(20):
    cfg = propose(hist)
    score = run_and_score(cfg)
    db.execute("INSERT INTO trial(cfg, score) VALUES (?,?)", (json.dumps(cfg), score))
    db.commit()
    hist.append((cfg, score))
    print(f"cfg={cfg} score={score:.4f}")
$ python loop.py
cfg={'lr': 0.0005} score=0.8123
cfg={'lr': 0.00025} score=0.8340
cfg={'lr': 0.000125} score=0.8291

여기서 for _ in range(20)을 순차 실행이 아니라 수천 개 병렬로 바꾸고, propose를 프론티어 모델로 바꾸고, run_and_score를 클러스터 잡 제출로 바꾸면 — 원문이 말하는 그림이 된다. 어려운 건 아이디어가 아니라 병렬도를 3자리수로 올렸을 때 무너지는 주변부다.

3. 실무 관점: 병렬도를 올리면 어디가 먼저 터지나

(1) 스케줄링 — GPU는 CPU처럼 오버커밋되지 않는다

사내에서 튜닝 잡 200개를 한 번에 던지면 제일 먼저 만나는 화면이다.

$ kubectl describe pod trial-0147
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  3m    default-scheduler  0/8 nodes are available: 8 Insufficient nvidia.com/gpu.
           preemption: 0/8 nodes are available: 8 No preemption victims found for incoming pod.

Pending이 수백 개 쌓이면 스케줄러 큐가 지저분해지고, 팀 동료의 대화형 노트북 잡까지 밀린다. 대응은 셋 중 하나다.

  • 큐 자체를 분리한다. Kueue / Volcano / Slurm 파티션으로 "탐색용 저우선순위 큐"를 따로 두고 PriorityClass에 preemption을 허용한다.
  • 동시 실행 상한을 루프 쪽에서 건다. Argo Workflows의 parallelism, Slurm array의 %N 같은 것. 큐에 던지는 개수와 실제 도는 개수를 분리하는 게 핵심이다.
  • 실험 하나가 GPU를 다 안 쓰면 MIG나 time-slicing으로 쪼갠다. 다만 스루풋 측정값이 흔들리므로 성능 비교 실험에는 쓰지 않는다.
# Slurm array로 동시 16개만 유지하며 512개 trial 굴리기
$ sbatch --array=1-512%16 --gres=gpu:1 --time=00:30:00 trial.sbatch
Submitted batch job 918234

$ squeue -u $USER -h -o "%t" | sort | uniq -c
     16 R
    496 PD

(2) 아티팩트 스토리지와 메타데이터 DB

trial 하나가 체크포인트·로그·텐서보드 이벤트를 남긴다. 1000개면 그게 1000배다. 오브젝트 스토리지에 초당 수백 개 PUT을 때리면 이런 걸 본다.

botocore.exceptions.ClientError: An error occurred (SlowDown) when calling the
PutObject operation (reached max retries: 4): Please reduce your request rate.

실전 처방: ① 작은 파일 다발을 tar로 묶어 한 번에 올린다, ② 프리픽스에 해시를 넣어 샤딩한다, ③ 중간 체크포인트는 기본적으로 버린다(상위 N개 trial만 재현용으로 보존). 그리고 MLflow/W&B 같은 트래킹 서버의 DB도 병목이 된다 — 메트릭을 스텝마다 동기 전송하면 트래킹 서버가 먼저 죽는다. 배치 로깅으로 바꾸는 게 첫 수순이다.

(3) 평가가 흔들리면 루프가 거짓말을 학습한다

개인적으로 가장 비싸게 배운 부분. 자동화 루프는 "점수를 올리는 방향"으로만 움직이는데, 그 점수가 오염돼 있으면 루프는 오염을 최적화한다. 실제로 겪은 패턴들:

  • 테스트셋 누수. 제안기가 데이터 전처리 코드까지 수정할 권한을 갖는 순간, 검증셋이 학습셋에 섞이는 변경을 "성능 개선"으로 발견해버린다.
  • 노이즈보다 작은 개선. 시드 하나로 측정하면 ±0.5%p 흔들리는 지표에서 0.2%p 개선을 진짜라고 믿는다. 최소 3시드 중앙값, 아니면 이전 최고 기록과의 차이가 표준편차를 넘을 때만 채택하는 게이트가 필요하다.
  • 타임아웃 = 실패 = 낮은 점수 처리. 클러스터가 붐빌 때 죽은 잡이 "나쁜 설정"으로 기록되면, 제안기는 클러스터 혼잡도를 학습한다. 인프라 실패와 실험 실패는 반드시 다른 상태 코드로 구분해야 한다.

(4) 코드 생성이 들어올 때의 격리

LLM이 제안기 자리에 앉으면 제안 내용이 하이퍼파라미터가 아니라 실행 가능한 패치가 된다. 그 순간 이건 MLOps 문제가 아니라 보안 문제다. 네트워크 egress 차단(패키지 미러만 허용), 읽기 전용 데이터 마운트, seccomp 프로파일, 잡별 서비스 어카운트 토큰 비활성화 — CI 러너에 이미 적용해둔 규칙을 그대로 가져오면 된다. 클라우드 크리덴셜이 붙은 노드에서 생성 코드를 그냥 돌리는 건, 인터넷에서 받은 스크립트를 프로덕션에서 sudo로 실행하는 것과 같다.

(5) 지금 당장 쓸 수 있는 대안

Discovery Loop는 아직 쓸 수 있는 제품이 없다. 반면 루프 자동화의 80%는 이미 오픈소스로 가능하다. 탐색 알고리즘은 Optuna/Ray Tune, 워크플로 오케스트레이션은 Argo Workflows나 Slurm, 추적은 MLflow. 여기에 "제안기만 LLM으로 교체"하는 게 현실적인 진입 경로다. 순서를 바꾸면(LLM 먼저 붙이고 인프라 나중) 십중팔구 실패한다 — 평가 하네스와 잡 스케줄링이 안정적이지 않은 상태에서 제안 속도만 올리면, 쓰레기를 더 빨리 만들 뿐이다.

4. 정리

한 줄 요약: Discovery Loop는 "실험의 제안–실행–평가–반영 루프를 사람 손에서 떼어내겠다"는 미션 선언이고, 그 승부처는 모델이 아니라 스케줄러·스토리지·평가 하네스라는 인프라 쪽에 있다.

누가 언제 봐야 하나로 정리하면:

  • 지금 당장 할 것: 실험이 하루 열 개 이상 돌아가는 팀이라면, 측정 가능한 단일 스칼라 지표와 재현 가능한 실행 단위(컨테이너 + 고정 시드 + 잠긴 의존성)를 먼저 만들어라. 이게 없으면 어떤 자동화도 못 붙인다.
  • 주시만 할 것: 제품이 없으니 도입 검토 대상이 아니다. 다만 채용 공고와 향후 공개 자료를 보면 "그들이 실험 루프를 어떤 인프라로 감싸는지"에 대한 힌트가 나올 가능성이 있다. 그건 우리 파이프라인 설계에 그대로 쓸 수 있는 정보다.
  • 경계할 것: 자동화 루프의 결과를 사람이 검증 없이 머지하는 관행. 루프는 지표를 올리지, 문제를 이해하지 않는다.

MapReduce가 "분산 처리를 map/reduce 두 함수로 추상화"해서 판을 바꿨듯이, 이번에도 "실험을 propose/run/score 세 함수로 추상화"하는 그림으로 보인다. 그 추상화가 실제로 어디까지 유효한지는 공개된 결과물이 나와야 판단할 수 있다.

참고 자료

728x90

+ Recent posts