최명헌 Myeong Heon (Sean) Choi
신뢰성은 주장하는 게 아니라 재는 것입니다.
ML 플랫폼·LLM 인프라 엔지니어. AI 서비스가 올라가는 인프라를 직접 만들고, 재고, 운영합니다 — 관측성, Kubernetes, 그리고 GPU·LLM 추론의 신뢰성.
● 무엇을 만들었는지가 아니라, 얼마나 안정적으로 돌아가는지 · mooner92.info
다섯 개 프로젝트, 따로 표시하지 않은 것은 모두 혼자 설계하고 만들었습니다. 대부분은 한국환경연구원에서 실제로 돌아가고 있습니다 — 데모가 아니라 실사용자와 실제 GPU 위에서요. 신뢰성은 주장하는 게 아니라 재는 것이라고 생각합니다.
주장이 아니라 실측
| 지표 | 이전 → 이후 |
|---|---|
| 엄격 기준 검색 정확도 · Hit@1 | 60.0% → 82.9% |
| 답변 끝 면책 고지 보장 | 80.6% → 100% |
| 조용한 실패를 알아채기까지 걸린 시간 | 5.7일 → 40분 |
| 가속기 작업 실행 시간 | 기본 K8s 스케줄러 대비 약 70% 단축 |
| 부하 상태의 LLM 서빙 | 단건 p95 18초 · 4.3 req/min에서 포화 |
위 숫자는 전부 제가 직접 운영하는 시스템에서 잰 값이고, 확인용 명령어를 레포 README에 적어 뒀습니다.
프로젝트 01 — 프로덕션
사내 규정을 위한 온프레미스 RAG — 근거를 대거나, 아니면 답하지 않습니다.
행정·회계·감사에서는 틀린 답이 곧 사고입니다. 그래서 목표를 “그럴듯한 답”이 아니라 틀리지 않는 것으로 잡았습니다. 599개 문서를 조문 단위로 자르고, KURE-v1으로 임베딩해 밀집 검색 top-20을 뽑은 뒤, 온프레미스 리랭커로 top-5까지 좁히고, 생성 전에 신뢰 게이트를 통과시킵니다. 근거가 없으면 지어내지 않고 “규정에서 확인되지 않습니다”로 거부합니다.
FastAPI · Ollama (Qwen3.5-9B GGUF) · Chroma · KURE-v1 · bge-reranker-v2-m3 · Next.js 14 · MLflow · Cloudflare Zero Trust
82.9%
리랭커를 넣은 뒤의 엄격 Hit@1. 60.0%에서 올랐습니다.
100%
모든 답변 끝에 면책 고지가 붙습니다. 모델이 19% 확률로 빠뜨리던 것을 코드가 보장하게 바꿨습니다.
6,044
599개 문서에서 나온 청크. 상위법령 4,116청크는 물리적으로 분리해 둡니다.
[규정, 조문] 출처가 붙고, 근거 카드는 그 자리에서 펼쳐 볼 수 있습니다추측이 아니라 dev 인스턴스에 동시성 스윕을 직접 돌린 결과입니다. 처리량은 동시 요청 2개에서 이미 포화되고, 그 위로는 전부 큐잉으로 밀립니다. 생성 속도가 부하와 상관없이 14~15 tok/s로 일정하다는 것 — 이걸 보고 병목을 GPU 성능이 아니라 배칭 없는 직렬 생성으로 좁혔습니다.
| 동시성 | p50 | p95 | 처리량 | 생성 속도 |
|---|---|---|---|---|
| 1 | ~15초 | 18.0초 | 3.9 req/min | 13.9 tok/s |
| 2 | 28.3초 | 30.3초 | 4.3 req/min | 14.4 tok/s |
| 4 | 50.0초 | 55.2초 | 4.3 req/min | 15.0 tok/s |
| 8 | 74.9초 | 111.2초 | 3.8 req/min | 15.0 tok/s |
지금 쓰는 사내 수십 명 규모에는 충분합니다. 더 키워야 한다면 순서는 분명합니다 — continuous batching을 지원하는 서버로 옮기고, Vulkan에 묶어 두는 드라이버 제약을 풀고, 그다음이 replica입니다. 토큰 수치는 엔드포인트가 usage를 돌려주지 않아 문자 수 기반 근사입니다.
금액·비율·기간이 근거에 없으면 코드가 경고를 붙입니다. 모델 재량이 아닙니다. HWP 변환에서 깨진 표는 수치 인용 자체를 금지하고, 삭제·폐지된 조문은 근거에서 강등합니다.
평일 200문항, 주말 500문항×3회차를 자동으로 출제·답변·채점·게시하고 결과를 MLflow에 기록합니다.
BM25 하이브리드 검색은 실측에서 이득이 없어 기본 off로 내렸습니다. 같은 근거로 치른 모델 A/B에서 57.3% 대 40.8%가 갈렸고, 이 숫자가 지금 돌아가는 모델을 정했습니다.
사고 기록 — 버그는 지표 쪽에 있었습니다. 하루아침에 정답률이 90에서 68로 떨어졌습니다. 서비스가 나빠진 게 아니었습니다. 출제 표본이 매일 85%씩 갈리고 있어서, 합산 정답률로는 애초에 개선을 증명할 수 없는 구조였던 겁니다. 재시험 문항과 신규 문항 코호트를 갈라 보니 같은 문항의 오답이 7건에서 1건으로 줄어 있었고, 급락이 표본 탓이라는 걸 30분 만에 짚어냈습니다. 이후로도 측정기가 틀린 사고가 이어졌습니다 — 거부 판정 정규식이 정답을 거부로 세고, 채점기가 사흘간 정답을 ‘부분 정답’으로 깎고, 거부 원인 56건 중 9건이 엉뚱하게 ‘검색실패’로 분류되고 있었습니다. 다섯 건을 한 번에 묶어 고친 뒤 25회차 4,800여 문항을 다시 집계해 156건이 이동하는 것을 확인했습니다.
프로젝트 02 — 대표작 · 진행 중
온프레미스 GPU 플릿 관측·인시던트 플랫폼.
LLM 추론을 돌리는 A40 2장짜리 호스트를 포함해 연구용 서버 5대를 운영하는데, 플릿 전체의 상태·용량·장애를 한눈에 볼 곳이 없었습니다. 모니터링은 호스트마다 따로 도는 컨테이너가 전부였습니다. KEIwi는 그 단일 창구입니다. 중앙 Prometheus와 Grafana를 두고, 그 위에 읽기 전용 Next.js 콘솔을 얹었습니다.
Prometheus · Grafana · DCGM · OpenSearch / Logstash / Filebeat · GlitchTip · Ansible · Next.js 16 · Cloudflare Access
16,633
스크랩 타깃 20개와 GPU 6장에서 나오는 활성 시계열.
32.0M
인덱스 150개에 쌓인 로그 문서. 4개 노드에서 실시간으로 들어옵니다.
205배
조용한 실패를 알아채는 속도 — 5.7일에서 약 40분으로.
모든 노드의 up / down / no-data를 한눈에 봅니다. 근거는 단일 진실원천인 inventory.yaml 하나입니다.
직접 닿지 않는 노드는 노드별 SSH 터널로 스크랩하되 instance 라벨을 실제 주소로 되돌립니다. 메트릭이 없으면 거짓 down이 아니라 no-data로 표시합니다.
디스크 80%, 메모리 10%, GPU 85°C 같은 흔한 기본값은 이 플릿의 실제 baseline에서 상시 발화했습니다. 그래서 전부 자체 30일 분포에서 다시 뽑았습니다.
사고 기록 — 대시보드는 5.7일 동안 초록이었습니다. 로그 인입이 멈춰 있었는데 아무도 몰랐습니다. 알림이 아니라 우연한 조회로 드러났습니다. 원인은 서로 무관한 결함 둘이었습니다. 수집 에이전트가 지원하지 않는 설정 키 하나가 이벤트를 조용히 전멸시켰고, 라이브 워킹트리에서 친 git checkout이 운영 중인 파이프라인 리로드를 유발했습니다. 고친 건 설정이 아니라 구조입니다 — dead man’s switch를 놓아 인입이 정상일 때만 ping을 보내게 했고, 그 경로는 Grafana가 죽어도 살아 있습니다. 여기에 관측을 세 층으로 갈랐습니다. 인프라 지표, 앱 예외, 그리고 관측 스택 자체의 침묵. 이후 같은 모양의 조용한 실패가 두 건 더 나왔습니다 — 6일간 아무도 몰랐던 드라이버 mismatch, 그리고 up=1인 채로 47만 번 크래시루프를 돈 프로세스. 그래서 원칙이 하나 생겼습니다. up은 프로세스가 살아 있다는 뜻이지, 서비스가 정상이라는 뜻이 아니다.
그다음 — 조치를 사다리로 나눴습니다. “어디까지 자동으로 고칠 것인가”를 한 번에 정하지 않고 L0부터 L4까지 단계로 쪼갰습니다. L1은 조치를 제안만 하고 실행하지 않습니다. L2는 승인을 받아 실행하고 감사 원장에 남깁니다. 이걸 만들다가 원장 기록이 실패해도 실행은 그대로 진행되던 fail-open 구멍 3건을 찾아 막았습니다. 남길 수 없으면 하지 않습니다. 단계마다 CI 게이트를 붙였습니다. 사다리를 우회하는 코드가 들어오면 빌드가 막힙니다.
프로젝트 03 — 게재 · KIISE 2024 · 제1저자
가속기가 어디 있는지 아는 node-affinity.
베어메탈 라즈베리파이 4노드 클러스터, 그중 둘에만 Coral Edge TPU가 달려 있습니다. 이 클러스터에서 기본 스케줄러는 트래픽만 보고 가속기 워크로드를 비가속 노드에 태연히 올립니다. 그러면 CPU 폴백이 걸려 약 10배 느려집니다. TPU 서비스를 node-affinity로 묶어 두면 추론이 실리콘이 있는 자리에 머무릅니다.
Kubernetes 1.28 · containerd · Nginx Ingress · Coral Edge TPU · TFLite · Prometheus · Grafana
약 70%
트래픽 기반 기본 스케줄러 대비 평균 작업 실행시간 단축 — 이미지 200장 × 20회 반복 측정.
| 서비스 | 파드가 올라간 노드 | 평균 처리 시간 |
|---|---|---|
| yes-tpu — node-affinity | Node 2 · Node 3 (Edge TPU) | 30.0초 · 30.2초 |
| no-tpu — 기본 스케줄러 | Node 1 — CPU 폴백 | 305.5초 |
| no-tpu — 기본 스케줄러 | Node 2 · Node 3 | 30.0초 · 30.3초 |
KIISE 2024에 제1저자로 게재했습니다. KEIwi의 직계 조상입니다 — 같은 문제의식을 실험실 규모에서 프로덕션 GPU로 옮긴 것입니다.
프로젝트 04 — 프로덕션
채용 심사의 이해충돌 탐지 — 초안만 만들고, 판단은 하지 않습니다.
심사위원이 형식이 뒤섞인 지원 서류에서 지도교수·공저자·과거 협업자를 일일이 손으로 찾아야 했습니다. MineSweeper는 온프레미스 비전 언어 모델로 관계자를 뽑아 근거가 붙은 초안을 사람에게 넘깁니다. 지원자 본인의 기록이 본인에게 불리하게 잡히지 않도록 자기 제외도 자동으로 처리합니다.
Qwen2.5-VL-7B · 온프레미스 GPU · Next.js · Prometheus exporter · 순수 Node HWP / HWPX 파싱
약 50
실제 지원 아카이브 13건에서 정확히 추출한 관계자 수.
3
형식이 섞인 문서 — PDF, HWP, 이미지. 도장·서명 영역은 확인 대상으로 표시합니다.
177
테스트. zip-slip과 업로드 크기 가드 포함. 외부 변환기는 쓰지 않습니다.
결정적 추출기가 실제 스캔 문서에서 결과 0건을 낸 뒤 비전 언어 모델로 갈아탔습니다.
추출 백엔드는 문서 종류별로 바꿔 낄 수 있습니다. 더 나은 모델이 나오면 수집·집계를 건드리지 않고 교체합니다.
GPU 할당 현황을 직접 만든 Prometheus exporter와 Grafana 대시보드로 봅니다.
프로젝트 05 — 개인
에이전트 셋, 칸반 보드 하나, 직접 대화는 없음.
conductor·architect·executor 세 에이전트가 서로에게 메시지를 보내지 않고 공유 칸반 보드만 보고 조율합니다. 레포를 포크해 개선안을 세우고, 두 갈래로 나눠 구현·리뷰한 뒤 PR을 엽니다. 전 과정이 로컬 추론입니다.
로컬 vLLM (Qwen2.5-Coder-32B) · SQLite 칸반 · FastAPI · Next.js 14
fan-out / fan-in 지점을 명시하고 리뷰 게이트를 둘 두었습니다. 리뷰 실패는 정당한 종료 상태입니다 — 조용히 통과시키는 대신 사람에게 블록을 겁니다.
코드는 전용 봇 계정의 PR로만 올라옵니다. 최종 병합은 사람 몫으로 남겨 뒀습니다 — 의도적으로 그은 경계입니다.
모니터링 대시보드가 쓰는 건 ‘세션 시작’과 ‘태스크 언블록’ 둘뿐이고, 그마저 동일 출처 프록시 뒤에 있습니다. 브라우저는 내부 포트와 직접 말하지 않습니다.
신뢰성은 형용사가 아니라 숫자입니다 — p95, 에러 예산, 검색 정확도. 안 재 본 것은 ‘괜찮다’가 아니라 ‘아직 안 쟀다’라고 말합니다.
지표도 하나의 시스템이고, 고장 나 있는 게 지표 쪽일 수 있습니다. 실제로 몇 번 그랬습니다.
결정은 ADR로 적어 둡니다. 그래야 리뷰할 수 있고, 되돌릴 수 있습니다.
모르는 상태는 거짓 경보가 아니라 ‘no-data’입니다. 기계는 초안을 만들고, 판단은 사람이 합니다.
온프레미스, Zero Trust 뒤 노출, k-익명 집계, 코드 밖의 시크릿.
재 보고 접은 방법은 채택한 방법만큼 값어치가 있습니다. 둘 다 기록으로 남깁니다.