Myeong Heon (Sean) Choi
ML platform and LLM infrastructure engineer. I build, measure and operate the infrastructure AI services run on — observability, Kubernetes, and GPU / LLM-inference reliability.
● not what I built — how reliably it runs · mooner92.info
Five projects, designed and built solo unless noted. Most run in production at the Korea Environment Institute — on real users and real GPUs, not as demos. Reliability is treated as something measured, not claimed.
Measured — not claimed
| Metric | Before → after |
|---|---|
| Answers carrying the closing disclaimer | 80.6% → 100% |
| Strict retrieval accuracy · Hit@1 | 60.0% → 82.9% |
| Silent-failure detection time | 5.7 days → 40 min |
| Accelerator task execution time | ~70% faster than the default K8s scheduler |
| LLM serving under load | p95 18s single · saturates at 4.3 req/min |
Every number above was measured on a system I run, with the verification command written into the repo README.
Project 01 — Production
On-prem RAG for internal regulations — every answer cites its source, or refuses.
In administration, finance and audit, a wrong answer is an incident. So the goal was never "sounds plausible" — it was not being wrong. 599 documents are chunked at the article level, embedded with KURE-v1, retrieved dense top-20, reranked on-prem to top-5, then passed through a trust gate before generation. If the evidence isn't there, it refuses.
FastAPI · Ollama (Qwen3.5-9B GGUF) · Chroma · KURE-v1 · bge-reranker-v2-m3 · Next.js 14 · MLflow · Cloudflare Zero Trust
100%
of answers carry the closing disclaimer. The model dropped it 19% of the time; code guarantees it now. Up from 80.6%.
82.9%
strict Hit@1 after adding the reranker, from 60.0%.
6,044
chunks across 599 documents, plus 4,116 statute chunks kept physically separate.
[regulation, article] source, with evidence cards you can open in placeA concurrency sweep against the dev instance, not a guess. Throughput saturates at two concurrent requests; everything above that becomes queueing. Generation speed stays flat at 14–15 tok/s regardless of load — which is what isolates the bottleneck to non-batched, serial generation rather than raw GPU capacity.
| Concurrency | p50 | p95 | Throughput | Generation |
|---|---|---|---|---|
| 1 | ~15 s | 18.0 s | 3.9 req/min | 13.9 tok/s |
| 2 | 28.3 s | 30.3 s | 4.3 req/min | 14.4 tok/s |
| 4 | 50.0 s | 55.2 s | 4.3 req/min | 15.0 tok/s |
| 8 | 74.9 s | 111.2 s | 3.8 req/min | 15.0 tok/s |
Enough for the few dozen internal users it serves. If it needed to scale, the order is clear: move to a server with continuous batching, then resolve the driver constraint pinning it to Vulkan, then add replicas. Token figures are character-based approximations — the endpoint doesn't return usage.
Amounts, percentages and deadlines missing from the evidence get a warning attached by code, not at the model's discretion. Tables broken by HWP conversion are barred from being quoted numerically. Repealed articles are demoted as evidence.
200 questions on weekdays, 500 × 3 rounds at weekends — generated, answered, scored and published automatically, with results tracked in MLflow.
Hybrid BM25 retrieval measured no gain, so it ships off by default. A model swap A/B on identical evidence — 57.3% vs 40.8% — decided which model runs.
War story — the metric was the bug. Daily accuracy fell 90 → 68 overnight. The service hadn't regressed: 85% of the question sample rotates each day, so the aggregate could never prove improvement in the first place. Splitting retake and new-question cohorts showed errors on repeated questions going 7 → 1, and identified the drop as a sampling artefact within 30 minutes. Two separate measurement bugs surfaced the same way — a refusal regex counting correct answers as refusals, and a grader marking exact answers "partial" for three days.
Project 02 — Flagship · in progress
On-prem GPU fleet observability and incident platform.
A five-server research cluster — including a dual-NVIDIA-A40 host running LLM inference — had no unified view of health, capacity or incidents; monitoring existed as disconnected per-host containers. KEIwi is the single pane of glass: central Prometheus and Grafana behind a read-only Next.js console.
Prometheus · Grafana · DCGM · OpenSearch / Logstash / Filebeat · GlitchTip · Ansible · Next.js 16 · Cloudflare Access
16,633
active time series across 20 scrape targets and 6 GPUs.
32.0M
log documents across 150 indices, ingesting live from four nodes.
205×
faster detection of a silent failure — 5.7 days down to about 40 minutes.
Up / down / no-data across every node at a glance, backed by a single inventory.yaml source of truth.
Unreachable nodes are scraped over per-node SSH tunnels with instances relabeled to real addresses; missing metrics report no-data instead of a false down.
Textbook defaults — 80% disk, 10% memory, 85°C GPU — all fired continuously against this fleet's real baseline. Every threshold is re-derived from its own 30-day distribution.
War story — the dashboard was green for 5.7 days. Log ingestion had stopped and nobody knew; it surfaced through an incidental query, not an alert. Two independent faults: a config key silently unsupported by the shipping agent, and a git checkout in a live working tree triggering a pipeline reload. The fix wasn't a patch — it was a dead man's switch that only pings while ingestion is healthy, on a path that survives Grafana itself dying, plus a three-layer split: infrastructure metrics, application exceptions, and the silence of the observability stack itself. Two more silent failures surfaced the same way afterwards — a driver mismatch unnoticed for six days, and a process reporting up=1 through 470,000 crash-loops. Hence the rule: up means the process exists, not that the service works.
Project 03 — Published · KIISE 2024 · first author
Node-affinity that knows where the accelerators are.
On a bare-metal, four-node Raspberry Pi cluster — two nodes fitted with Coral Edge TPUs — the default traffic-based scheduler happily places accelerator workloads on non-accelerator nodes, forcing a ~10× CPU fallback. Constraining the TPU service with node-affinity keeps inference where the silicon is.
Kubernetes 1.28 · containerd · Nginx Ingress · Coral Edge TPU · TFLite · Prometheus · Grafana
~70%
cut in average task-execution time with node-affinity vs. the traffic-based default — 200 images × 20 rounds.
| Service | Pod landed on | Avg. processing time |
|---|---|---|
| yes-tpu — node-affinity | Node 2 · Node 3 (Edge TPU) | 30.0 s · 30.2 s |
| no-tpu — default | Node 1 — CPU fallback | 305.5 s |
| no-tpu — default | Node 2 · Node 3 | 30.0 s · 30.3 s |
Published at KIISE 2024 as first author. Direct ancestor of KEIwi — the same idea, now on real GPUs in production.
Project 04 — Production
Conflict-of-interest detection for recruitment review — it drafts, it never decides.
Reviewers had to find advisors, co-authors and prior collaborators by hand across applicant documents in mixed formats. MineSweeper extracts related parties with an on-prem vision-language model and hands a sourced draft to a human — with automatic self-exclusion so an applicant's own record never counts against them.
Qwen2.5-VL-7B · on-prem GPU · Next.js · Prometheus exporter · pure-Node HWP / HWPX parsing
~50
related parties correctly extracted across 13 documents in a real applicant archive.
3
mixed formats — PDF, HWP, images — with stamp / signature regions flagged for review.
177
tests, plus zip-slip and upload-size guards. No external converter.
A vision-language model replaced a deterministic extractor that returned zero results on real scanned documents.
The extraction backend is swappable per document class, so better models drop in without touching ingest or aggregation.
GPU allocation is visualized through a custom Prometheus exporter and a Grafana dashboard.
Project 05 — Personal
Three agents, one Kanban board, zero direct messages.
Conductor, architect and executor coordinate entirely through a shared Kanban board — never messaging each other directly — to fork a repo, plan improvements, implement and review in two parallel lanes, and open a pull request. All on local inference.
Local vLLM (Qwen2.5-Coder-32B) · SQLite Kanban · FastAPI · Next.js 14
Explicit fan-out / fan-in points and two review gates — a failed review is a legitimate terminal state that blocks for a human rather than silently passing.
Code lands as pull requests from a dedicated bot account; final merge stays with a human operator — a deliberate human-in-the-loop boundary.
The monitoring dashboard's only writes are "start a session" and "unblock a task", behind a same-origin proxy — the browser never talks to internal ports.
Reliability is a number — p95, error budgets, retrieval accuracy — not an adjective. What isn't measured gets called unmeasured, not "fine".
A metric is a system too, and it can be the thing that's broken. Twice it was.
Decisions are written down as ADRs, so they're reviewable and reversible.
Unknown state is "no-data", not a false alarm. Machines draft; humans decide.
On-prem, Zero-Trust exposure, k-anonymized aggregates, secrets out of code.
An approach measured and dropped is worth as much as one adopted — and both get written down.