MLflow는 ML Lifecycle의 experimentmodel 사이 경계를 메우는 도구다. experiment 쪽에서는 “어떤 파라미터로 무엇을 학습했는가"를, model 쪽에서는 “어느 버전이 지금 production을 가리키는가"를 붙잡아준다. 그 경계는 대형 ML 팀에만 있는 것이 아니다. 가벼운 Logistic Regression 모델 하나를 운영할 때도 똑같이 나타난다.

이 글은 MLflow 자체를 다룬다. 무엇이고, ML Lifecycle의 어디에 위치하며, 경량 팀이 그 중 어떤 조각을 고를 수 있는가.

ML Lifecycle

ML 프로젝트는 대체로 네 단계로 움직인다.

  1. Experiment — 데이터를 보고, 파라미터를 바꿔가며 모델을 학습해본다. 메트릭을 기록하고 돌아가서 다시 실행한다.
  2. Model — 쓸 만한 결과가 나오면 그 모델을 “이것이 지금 우리의 모델"이라고 선언한다. 버전과 lineage가 붙는다.
  3. Deployment — 그 모델을 서빙 환경에 배치한다. 롤아웃, 롤백, 트래픽 전환 같은 문제가 여기서 발생한다.
  4. Monitoring — 서빙 중인 모델의 드리프트와 성능 저하를 감시한다.

각 단계는 고유한 문제를 가진다. experiment는 “무엇을 해봤는지 기억하는 것"이 어렵고, model은 “지금 어느 것이 진짜인지” 합의하는 것이 어렵다. deployment는 “바꿔 끼우는 것"이 어렵고, monitoring은 “언제 다시 학습시켜야 하는지” 판단하는 것이 어렵다.

MLflow는 이 중 앞의 두 칸을 주로 메운다. deployment와 monitoring 영역에도 걸쳐 있지만, 중심축은 experiment와 model이다.

model_v3_final.pkl 문제

파일 시스템만으로 모델을 관리할 때 어디서 깨지는가를 먼저 확인해야 한다. 그래야 왜 경계에 도구가 필요한지 드러난다.

처음에는 간단하다. model.pkl을 S3에 올리고, 인퍼런스 서버가 그걸 읽는다. 학습이 끝날 때마다 덮어쓰면 된다.

그러다 한 번 롤백이 필요해진다. 어제 버전으로 돌아가야 하는데 파일은 이미 덮어써졌다. 그래서 model_v2.pkl, model_v3.pkl로 나누기 시작한다. 얼마 안 가 model_v3_final.pkl이 등장한다. 그다음은 model_v3_final_really.pkl이다.

이 이름들이 해결하지 못하는 것이 세 가지 있다.

  • Lineagemodel_v3_final.pkl이 어떤 코드로, 어떤 데이터로, 어떤 파라미터로 학습됐는지 추적할 방법이 없다. 재현하려 해도 같은 결과가 나오지 않는다.
  • Alias — “지금 production이 가리키는 모델"을 코드 외부의 문자열 규칙으로 관리하게 된다. 인퍼런스 서버가 latest.pkl을 읽도록 할지, 환경 변수로 버전을 주입할지, 매번 결정해야 한다.
  • 재현성 — 몇 달 뒤에 같은 실험을 실행하고 싶은데, 그때의 파라미터와 코드를 모은 기록이 어디에도 없다.

이 세 가지를 풀려면 결국 “파일 이름” 레이어 위에 메타데이터 레이어 하나가 필요해진다. 그게 MLflow가 메우는 자리다.

MLflow의 네 조각

MLflow는 서로 독립적인 네 개의 컴포넌트로 구성된다. 전부 하나의 패키지 안에 있지만, 쓰는 쪽에서 골라 쓸 수 있다.

Tracking

학습 한 번을 run이라는 단위로 기록한다. 파라미터, 메트릭, 그리고 학습 결과물(모델 파일, 플롯, 로그)을 run에 묶어둔다. 여러 run은 experiment라는 이름으로 묶인다.

import mlflow

with mlflow.start_run():
    mlflow.log_param("C", 0.1)
    mlflow.log_metric("val_auc", 0.782)
    mlflow.sklearn.log_model(model, "model")

이 한 덩어리가 lineage의 씨앗이다. 몇 달 뒤에 “그때 val_auc가 0.78이었는데 파라미터가 뭐였지?“를 물어볼 수 있는 기록이 된다.

Model Registry

Tracking이 “어떻게 학습했는가"를 기록한다면, Registry는 “어떤 결과물을 우리 것으로 선언할 것인가"를 기록한다. 학습 결과물 중 하나를 registered model로 승격시키면 버전이 붙는다. v1, v2, v3가 자동으로 쌓인다.

그리고 그 버전들 위에 alias를 붙일 수 있다. champion이라는 alias는 특정 버전을 가리키는 mutable reference다. 새 버전이 검증을 통과하면 champion alias를 옮긴다. 코드를 바꾸지 않고, 이름 규칙을 바꾸지 않고, alias 하나만 이동시키는 것으로 “production이 가리키는 모델"이 교체된다.

mlflow.register_model("runs:/<run-id>/model", name="ctr-model")
client.set_registered_model_alias("ctr-model", "champion", version=7)

Registry는 앞서 말한 model_v3_final.pkl 문제를 모두 치운다. lineage는 run과 자동 연결되고, alias는 이름 규칙을 대체하고, 재현은 run id로 가능해진다.

중요한 제약이 하나 있다. Registry를 쓰려면 DB backend가 필수다. 파일 스토리지(./mlruns)만으로는 registry API가 동작하지 않는다. 가볍게 시작하고 싶어도 PostgreSQL이나 MySQL, 최소한 SQLite 하나는 띄워야 한다. MLflow 3.7.0부터 default backend가 SQLite로 바뀌어서 처음 진입 장벽이 조금 낮아졌다.

Models

“모델 파일"이 무엇인지 표준화하는 조각이다. sklearn, pytorch, xgboost 같은 프레임워크마다 flavor가 있고, 같은 모델을 여러 flavor로 저장할 수 있다. 저장된 모델은 로드할 때 원래 프레임워크 코드 없이도 불러올 수 있다.

Models는 experiment와 deployment 사이를 이어주는 포터빌리티 계층이다. Tracking/Registry가 “어떤 모델이냐"를 다룬다면, Models는 “그 모델을 어떻게 직렬화하느냐"를 다룬다.

Projects

MLproject 파일과 conda/docker 설정을 묶어서 “누가 돌려도 같은 환경"을 만든다. mlflow run .으로 실행하면 환경이 세팅되고 학습이 실행된다.

네 조각 중 가장 덜 쓰이는 편이다. 이미 내부에 배치 실행 표준이 있는 팀은 Projects를 덮어쓰지 않고 자기 표준을 유지한다.

Lifecycle 배치

flowchart LR
    E[Experiment] -->|"Tracking
(run, param, metric)"| M[Model] M -->|"Registry
(version, alias)"| D[Deployment] M -.->|"Models
(flavor)"| D P[Projects] -.->|"실행 환경"| E D --> Mo[Monitoring]
  • Tracking: experiment 단계 내부
  • Registry: experiment와 deployment 사이의 model 칸
  • Models: model에서 deployment로 넘어가는 포터빌리티 축
  • Projects: experiment 칸의 재현성 계층 (선택)
  • monitoring은 MLflow가 직접 담당하지 않는다. 별도 도구가 필요하다.

이 그림이 MLflow의 범위를 가장 간결하게 보여준다. 네 조각이 각자의 자리에 있고, 어느 칸을 채울지는 프로젝트가 정한다.

Tracking 과 Registry 선택

경량 LR 모델을 production에 운영하는 시나리오를 가정해보자. 네 조각 중 자주 마주치는 조합은 둘이다. TrackingRegistry.

Tracking이 필요한 이유. 학습 배치가 매 주기 LR을 다시 실행하면, 그때마다 파라미터와 validation 메트릭이 달라진다. 어느 run이 어떤 숫자를 냈는지 나중에 추적해야 한다. 파일 이름으로 관리할 수 있는 수준의 기록이 아니다. Tracking이 메워주는 자리가 바로 이 지점이다.

Registry가 필요한 이유. 학습 배치가 만든 모델 중 검증 단계를 통과한 것만 “champion"으로 승격해야 한다. 인퍼런스 서버는 그 champion을 로드한다. 이걸 파일 규칙으로 하면 서버가 latest.pkl을 polling하게 되고, 검증이 안 끝난 모델이 먼저 올라가는 race가 생긴다. alias를 쓰면 그 race가 사라진다. 배포의 방아쇠를 당기는 주체와 배포되는 객체가 깔끔히 분리된다.

alias가 움직이는 것과 실제 인퍼런스 서버 교체는 별개의 사건이다. champion alias가 옮겨진 뒤 배포 도구(예: Argo Rollouts)가 POD 교체를 트리거한다. Rollouts가 새 POD를 띄우면, 그 POD는 기동 시 champion alias가 가리키는 모델을 로드해서 서비스에 투입된다. MLflow는 “어느 것이 champion인가"까지만 말하고, “어떻게 서비스에 배치할 것인가"는 배포 도구의 몫이다.

이 분리가 핵심이다. MLflow가 모든 것을 할 필요는 없다. 경계만 메우면 된다.

사용하지 않는 컴포넌트

Models 포맷은 Tracking에 모델을 로깅할 때 자동으로 따라온다. 명시적으로 고르는 조각은 아니지만 혜택은 받는다. Registry에서 runs:/<id>/model URI로 꺼낼 수 있게 되는 것이 이 포맷 덕분이다.

Projects는 잘 쓰이지 않는다. 팀이 이미 안정적인 배치 실행 표준을 가지고 있다면, 그 위에 MLproject 레이어를 추가하는 것은 중복이다. 한 배치가 한 프레임워크 안에서 실행되면 Projects의 재현성 이득은 크지 않다.

Serving도 선택적이다. MLflow는 자체 서빙 엔드포인트(mlflow models serve)를 제공하지만, LR 같은 경량 모델의 인퍼런스는 기존 서버에서 sklearn으로 직접 처리하는 쪽이 더 가볍고, 기존 인프라에 통합하기도 쉽다. 서빙 레이어를 MLflow에 위임할 이유가 없는 경우가 많다.

네 조각 중 두 개만 쓴다고 해서 MLflow를 “반만 쓴” 것은 아니다. 메워야 할 경계만 메우고 나머지는 다른 도구에 맡기는 것이 이 도구의 정석적인 사용 방식에 가깝다.

맺음

경계에 부딪힌다고 했다. 그 경계는 파일 이름이 설명하지 못하는 meta 정보(언제, 어떻게, 무엇으로, 지금 어느 것이 진짜인가)가 쌓이기 시작하는 지점이다. MLflow는 그 지점에 놓이는 가벼운 메타데이터 레이어다. 얼마나 가볍게 쓸지는 프로젝트가 정한다.

대형 ML 팀에만 있는 도구가 아니다. LR 하나를 운영하더라도 같은 경계는 찾아온다. 그때 필요한 칸만 골라 채우면 된다.