forecaster 아이디어가 구체화된 뒤, 프로젝트는 ML 문제라기보다 data engineering 문제처럼 느껴지기 시작했다. 하지만 그것은 후퇴가 아니었다. 오히려 이후 model이 의미를 가질 수 있는지 결정하는 단계였다.

Borg trace는 친절한 product analytics table이 아니다. usage window, task event, machine information, scheduling metadata, terminal state, generated artifact가 모두 서로 다른 의미를 가진다. feature를 준비하는 동안 그 의미가 조금만 흔들려도 model은 여전히 학습되고, chart는 여전히 그럴듯하게 보이고, 결론은 여전히 틀릴 수 있다.

그래서 이 단계의 목표는 지루한 부분을 명시적으로 만드는 것이었다. raw data는 어디에 있고, processed data는 어디에 있고, join은 어떻게 수행되고, label은 어떻게 보존되고, 각 run의 artifact는 나중에 어떻게 다시 검토할 수 있는지 정리해야 했다.

외부 processed tree

raw data와 processed data는 ~/Documents 아래에 두었다. repository가 generated file 쓰레기장이 되면 안 된다고 생각했기 때문이다. baseline path와 advanced XGBoost workspace도 분리했다. 이 결정 덕분에 나중에 실험할 때 한 track만 다시 만들고 다른 track은 망가뜨리지 않을 수 있었다.

~/Documents/borg_data
~/Documents/borg_processed

~/Documents/borg_xgboost_workspace/raw
~/Documents/borg_xgboost_workspace/processed
~/Documents/borg_xgboost_workspace/models
~/Documents/borg_xgboost_workspace/reports
~/Documents/borg_xgboost_workspace/runtime
~/Documents/borg_xgboost_workspace/config

겉으로는 정리정돈처럼 보이지만, 실제 작업 방식이 달라졌다. repair script나 retraining command를 실행했을 때, 해당 artifact가 첫 baseline task에 속한 것인지 advanced model track에 속한 것인지 바로 구분할 수 있었다. multi-horizon XGBoost model과 orchestrator trace를 동시에 만들기 시작하자 이 구분은 꽤 중요해졌다.

usage, event, machine join하기

첫 번째 실제 dataset은 joined table이었다. usage row에는 time-window behavior가 들어 있었다. event는 terminal state와 task lifecycle evidence를 제공했다. machine은 context를 더했다. 모든 것을 memory에 올려놓고 잘되길 바라는 대신 parquet을 streaming scan하고 싶어서 Polars를 사용했다.

usage = pl.scan_parquet(str(usage_path))
events = pl.scan_parquet(str(events_path))
machines = pl.scan_parquet(str(machines_path))

dataset = (
    usage
    .join(events, on=["collection_id", "instance_index"], how="left", suffix="_event")
    .join(machines, on="machine_id", how="left", suffix="_machine")
    .collect(engine="streaming")
)

dataset.write_parquet(output_path)

이 join은 계속 의심해야 하는 지점 중 하나였다. parquet write가 성공했다고 해서 dataset이 올바르다는 뜻은 아니었다. row count, positive label rate, missing machine id, 그리고 join 이후에도 terminal event timing이 말이 되는지 확인했다.

바로 advanced feature로 넘어가지 않은 이유

복잡한 feature set을 바로 만들고 싶은 유혹이 있었다. rolling window, delta, request ratio, priority encoding, cluster-level pressure 같은 것들은 join을 검증하는 일보다 훨씬 흥미로웠다. 하지만 너무 일찍 그쪽으로 가면 이후의 모든 오류를 분리해내기가 어려워졌다. 그래서 첫 track은 의도적으로 단순하게 유지했다. joined dataset을 만들고, forecaster frame을 만들고, baseline을 학습시키고, prediction을 살펴보는 것.

baseline은 최종 model이 아니었다. sanity instrument였다. baseline조차 그럴듯한 risk ranking을 만들지 못한다면, advanced model은 더 많은 parameter 뒤에 실패를 숨길 뿐이라고 봤다.

첫 baseline training loop

baseline training script는 forecaster frame을 표준화하고, validation data를 나누고, model을 학습시킨 뒤 나중에 반드시 필요할 output들을 썼다. validation prediction과 top risk alert였다. 이 파일들이 중요했던 이유는 aggregate metric만 보는 대신 실제 row를 확인할 수 있게 해줬기 때문이다.

python scripts/train_forecaster_baseline.py   --clusters b,c,d,e,f,g   --feature-profile baseline

의미 있는 output은 model file만이 아니었다. validation predictions parquet은 risk_score 기준으로 나중에 정렬해서 살펴볼 수 있는 ranked surface를 제공했다. top-risk alerts parquet은 이후 control-plane 아이디어가 된 것의 첫 버전이었다. risk는 action candidate에 붙일 수 있을 때 더 유용해진다.

  • validation_predictions.parquet: score가 매겨진 validation row. 나중에 risk_score 기준으로 정렬한다.
  • top_risk_alerts.parquet: inspection을 위해 남겨둔 high-risk row.
  • cluster forecaster parquet files: cluster별 feature/label frame.
  • metrics text and reports: 이후 실행과 비교할 수 있을 만큼의 context.

처음의 까다로운 trial-and-error loop

까다로웠던 부분은 pipeline을 조금만 바꿔도 downstream의 무언가가 무효화될 수 있다는 점이었다. optional column을 고치면 어떤 cluster의 feature coverage가 달라졌다. label horizon을 바꾸면 positive rate가 움직였다. schema issue를 수리하면 model result가 좋아 보이기도 했지만, 그 개선이 진짜인지 아니면 다른 filtered dataset 때문에 생긴 착시인지 다시 확인해야 했다.

이때부터 프로젝트 끝까지 이어진 습관이 생겼다. 모든 artifact는 inspectable해야 했다. model score만으로는 부족했다. dashboard만으로도 부족했다. local explanation이 없는 parquet file도 부족했다. 스스로를 속이지 않고 계속 앞으로 가려면, 프로젝트가 충분히 명시적이어야 했다.

이 단계가 남긴 것

이 단계가 끝났을 때 프로젝트에는 안정적인 baseline data path가 생겼다. Borg에서 파생한 joined dataset을 읽고, 15-minute-style failure target을 가진 forecaster frame을 만들고, baseline forecaster를 학습시키고, 내가 직접 확인할 수 있는 prediction을 쓸 수 있었다. 아직 orchestrator는 아니었다. 첫 번째로 믿을 만한 measuring tool이었다.

다음 문제는 더 고통스러웠다. schema drift와 broken label이었다. 그때부터 pipeline은 깔끔한 sequence가 아니라 repair project가 되었다.

이 단계의 산출물은 영리한 model이 아니었다. 이후 실험에 책임성을 부여하는 workspace였다. demo에서는 화려하지 않지만, 나중에 하는 모든 주장이 방어 가능한지 조용히 결정하는 기반이었다.