다키클라우드코리아
경력근거: 멀티테넌트 환경에서 성능 격리, SLA/SLO, 사용량 계측 및 자원 최적화를 구현합니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
'Token Factory는 단순히 LLM을 API로 배포하는 시스템이 아닙니다' 라는 문장은 모델 특성·하드웨어 구성·동시 요청·메모리 사용량·추론 지연과 비용을 종합적으로 최적화하는 플랫폼이라는 설명으로 이어집니다. 주요업무에는 '멀티테넌트 환경에서 성능 격리, SLA/SLO, 사용량 계측 및 자원 최적화를 구현합니다' 와 'TTFT, TPOT, Throughput, Goodput, 동시성 및 토큰당 비용을 측정하고' 가 같은 절에 있습니다. NVIDIA GPU 와 NPU 풀에 테넌트를 많이 올릴수록 한 테넌트가 몰아 보낸 요청이 다른 테넌트의 첫 토큰을 늦추고, 그 시간대를 고객 쪽 사유와 플랫폼 쪽 사유로 나누지 못하면 테넌트마다 SLO 를 약속할 근거가 서지 않습니다. 공고가 함께하고 싶은 사람으로 '같은 GPU에서 더 많은 요청을 안정적으로 처리하는 방법을 고민하는 분' 을 든 것도 같은 자리를 가리킵니다. 이 포트폴리오는 vLLM 에 직접 건 부하로 테넌트별 SLO 위반 원인을 계산하는 구현을 보여줍니다. NPU 런타임이나 GPU 커널 수준 최적화의 전문성은 주장하지 않습니다.
주요업무 2번에 '멀티테넌트 환경에서 성능 격리, SLA/SLO, 사용량 계측 및 자원 최적화를 구현합니다' 가 있습니다. 테넌트 계약표의 SLO·동시성·입력 상한을 요청 로그에 대어 SLO 를 넘긴 시간대가 고객 쪽 사유인지 격리가 깨진 탓인지 나누는 일이 이 문장의 성능 격리와 SLA/SLO 에 닿습니다.
같은 절에 'TTFT, TPOT, Throughput, Goodput, 동시성 및 토큰당 비용을 측정하고 지속적으로 개선합니다' 가 이어집니다. 스트리밍 응답에서 요청마다 첫 토큰 시간과 토큰 사이 시간을 재고, 요청 구간을 겹쳐 분 단위 동시 요청 수를 셉니다. 3주차 회귀 표에는 테넌트별 Goodput 이 들어갑니다.
자격요건은 vLLM 같은 추론 프레임워크를 쓰거나 내부를 분석한 경험과 GPU 메모리 특성에 대한 이해를 들고, 함께하고 싶은 사람으로 같은 GPU 에서 더 많은 요청을 안정적으로 처리하는 방법을 고민하는 분을 적습니다. GPU 한 장의 vLLM 에서 한 테넌트의 긴 입력이 KV 캐시를 채워 다른 테넌트가 대기열로 밀리는 상황을 직접 만들고, 그 분의 KV 캐시 사용률과 대기 요청 수를 판정 근거로 씁니다.
Token Factory 는 다키클라우드가 NVIDIA GPU 와 NPU 가속기 풀에서 여러 LLM 을 테넌트별 엔드포인트로 서빙하는 추론 플랫폼이고, 이 화면은 그 테넌트들의 SLO 리포트입니다. 고객사가 응답이 느려졌다고 문의한 날 기술 지원 담당자가 먼저 열어 그날의 시간대를 훑고, 원인이 정해진 시간대를 고객사 서비스 운영 담당자에게 보여 줍니다. 시간대는 테넌트·엔드포인트마다 한 시간씩 끊고, 요청 로그의 TTFT(요청을 보낸 뒤 첫 토큰이 나오기까지 걸린 시간)와 TPOT(그 뒤 토큰 하나가 나오는 간격)를 계약 SLO 에 견줍니다. SLO 를 넘긴 요청이 1% 를 넘은 시간대는 고객이 계약 동시성을 넘겨 보낸 경우, 계약보다 긴 입력을 몰아 보낸 경우, 둘 다 아닌데 같은 가속기 풀의 다른 테넌트에 밀린 경우로 나눠 보여줍니다. 동시 요청 수는 요청마다 도착 시각부터 마지막 토큰까지의 구간을 겹쳐 분 단위로 세고, 다른 테넌트는 이름 없이 요청 수로만 나옵니다.
다키클라우드 추론 플랫폼(Token Factory)에서 LLM 엔드포인트를 받아 상담 챗봇·사내 검색 같은 자사 서비스에 붙인 고객사(테넌트)의 서비스 운영 담당자와, 그 고객사를 맡은 다키클라우드 기술 지원 담당자. 응답이 느려졌다는 문의가 오면 고객은 플랫폼 쪽을, 플랫폼은 고객이 몰아 보낸 요청을 의심하며 로그를 따로 뒤진다. 월간 SLO 리포트 화면을 함께 보고 성능 격리가 깨진 시간대만 따로 다룬다
응답이 느리다는 문의가 오면 고객사는 플랫폼을, 기술 지원 담당자는 고객이 몰아 보낸 요청을 의심하고 각자 로그를 따로 찾아봅니다. 요청 로그에는 TTFT·TPOT·토큰 수가 요청마다 남지만, 그 요청이 들어온 순간 그 테넌트가 몇 개를 동시에 보내고 있었는지는 한 줄에 없어 사람이 시각을 맞춰 다시 셉니다. 요청마다 마지막 토큰이 나온 시각을 계산해 분 단위 동시 요청 수를 세고, SLO 를 넘긴 요청을 그 분의 동시성과 자기 입력 길이에 비춰 보면 고객 쪽 사유가 있는 시간대가 숫자로 나옵니다. 고객 쪽 사유가 없는 시간대에는 같은 풀의 KV 캐시 사용률·대기 요청 수·다른 테넌트 요청 수가 직전 한 시간보다 얼마나 올랐는지를 근거로 둡니다.
기술 지원 담당자가 날짜를 고르면 그날 테넌트·엔드포인트별 시간대가 네 판정 가운데 하나로 나오고, 성능 격리 실패 시간대를 펼치면 위반이 몰린 분의 KV 캐시 사용률·대기 요청 수와 다른 테넌트 요청 수가 직전 1시간 평균 옆에 나옵니다. 요청 로그와 gpu-pool-1 지표는 GPU 한 장을 몇 시간 빌려 vLLM 에 가상 테넌트 부하를 걸어 한 번 떠 두고, 판정과 화면은 그 파일로 돕니다. npu-pool-2 는 합성 값으로 시작합니다.
원인을 심은 48개 시간대에서 판정이 심은 원인과 맞은 수가 나오고, Go 게이트웨이에 테넌트별 동시성 상한을 걸고 다시 돌린 결과에서 처음 성능 격리 실패였던 시간대가 SLO 안으로 돌아왔는지 시간대마다 전후 판정이 한 줄에 보입니다.
k3s 에 올린 vLLM 의 설정을 바꿀 때마다 같은 시나리오가 밤에 다시 돌고, 다음 날 아침 테넌트별 Goodput(SLO 를 지킨 요청의 초당 처리 수)과 판정이 바뀐 시간대가 설정별 회귀 표로 나옵니다.
기술 지원 담당자가 10월 13일 리포트를 열면 gpu-pool-1 의 10:00~11:00 에서 고객사 C 상담 챗봇은 성능 격리 실패로, 같은 시각 고객사 F 배치는 계약 동시성을 넘겨 보냄으로 나오고, C 의 시간대를 펼치면 10:17 분의 KV 캐시 사용률 0.96·대기 요청 37건과 다른 테넌트 요청 2.5배가 직전 1시간 평균 옆에 나옵니다.
SLO 를 넘긴 요청이 400건 중 4건으로 꼭 1% 인 고객사 D, 입력이 계약 상한과 같은 8,000 토큰인 고객사 K 의 요청, 동시 요청 수가 계약과 같은 16 인 고객사 F 의 10:05 분, 풀 지표가 비어 있는 npu-pool-2 의 15:41 분을 섞어도 판정이 판정식대로 나오고, 지표가 빈 분은 판정을 바꾸지 않은 채 그 칸만 비워 둡니다.
날짜가 빠진 arrived_at('10:17:04'), 첫 토큰 전에 끊겨 output_tokens 가 0 인 요청, ttft_ms 칸에 '2,340ms' 처럼 글자가 든 줄, 한 시간 안에 다른 accel_pool 로 찍힌 엔드포인트의 줄, kv_cache_util 이 96 처럼 백분율로 들어온 분, 이 다섯 가지 깨진 입력을 섞어 올려도 화면은 열리고, 읽지 못한 줄은 줄 번호와 이유가 표 위 알림 줄에 쌓입니다.
원인을 심은 48개 시간대로 판정이 심은 원인과 맞은 수를 셉니다. 단, 시나리오와 정답을 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건에서의 동작입니다.
위 수치는 작성자가 GPU 한 장과 가상 테넌트로 만든 부하의 자가채점이며, 판정이 실제 원인과 맞는 비율은 다키클라우드 Token Factory 의 실제 테넌트 트래픽과 NPU 풀 지표가 있어야 잽니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태까지 만듭니다.
gpu-pool-1 의 요청 로그와 지표는 vLLM 에 실제로 부하를 걸어 잰 값이지만, 그 부하에서 고객사 F 가 계약을 넘겨 보내는 원인은 제가 부하 생성기에 심었습니다. npu-pool-2 의 로그·지표와 테넌트 계약표는 처음부터 합성한 값입니다. 48개 시간대에서 판정이 몇 개 맞든 그 일치 수는 제가 심은 원인을 제 판정식이 다시 읽어냈다는 데서 멈추고, 여러 원인이 한 시간대에 섞여 오는 실제 테넌트 트래픽에는 정답표가 없습니다.
고객사 D 는 400건 중 4건이라 '1% 를 넘으면' 을 '1% 이상' 으로 쓰면 걸립니다. req-k11-094012-0398 은 입력이 상한과 같은 8,000 토큰이라 긴 입력에 들지 않고, 고객사 F 의 10:05 분은 동시 요청 수가 계약과 같은 16 이라 넘은 분이 아닙니다. npu-pool-2 의 15:41 분은 지표가 비어 있어, 그 칸을 앞뒤 분 값으로 채우면 재지 않은 값을 근거로 내놓게 됩니다.
성능 격리 실패는 고객 쪽 두 사유가 아니라는 소거 판정입니다. 같은 분에 다른 테넌트 요청이 늘고 KV 캐시가 찼다는 사실은 함께 일어난 일이지 원인을 확정하지 않고, 같은 시각에 모델 재배포나 노드 장애가 있었다면 그것도 이 판정에 들어옵니다. 2주차에 테넌트별 동시성 상한을 걸고 다시 돌려 그 시간대가 SLO 안으로 돌아오는지 보는 일이 이 판정을 실험으로 맞대는 자리이고, 실제 도입에서는 배포 이력을 세 번째 근거로 더합니다.
TTFT·TPOT 를 부하 생성기 쪽에서 재면 네트워크 왕복 시간이 섞입니다. 1주차에는 vLLM 과 같은 VM 에서 부하를 걸어 그 몫을 줄이고, 2주차에 게이트웨이에서 잰 값과 견줘 차이를 적어 둡니다. 동시 요청 구간을 ttft_ms + tpot_ms × output_tokens 로 잡으면 실제 끝난 시각보다 토큰 하나 몫 길어지는데, 카드 판정식을 그대로 따르고 그 차이가 분 단위 판정을 바꾸는지는 2주차에 봅니다.
npu-pool-2 의 요청 로그와 지표는 NPU 장비 없이 합성했습니다. NPU 런타임은 배칭과 메모리 관리가 GPU 와 달라 같은 지표 이름이 같은 뜻인지부터 실장비로 다시 봐야 합니다. NPU 하드웨어별 최적화나 추론 엔진 내부를 고친 경험은 이 포트폴리오로 주장하지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
지원자가 L4 24GB GPU 한 장에 띄운 vLLM 에 가상 테넌트 여섯의 부하를 걸고, 부하 생성기가 요청마다 남긴 기록입니다. npu-pool-2 줄은 NPU 장비 없이 같은 모양으로 합성했습니다. 한 시간을 돌리면 수천 줄이 쌓이므로 여기에는 판정 샘플이 근거로 지목한 요청과 그 앞뒤의 평소 요청만 옮겼습니다. 고객사 F 가 계약 동시성 16 을 넘겨 보낸 10:16~10:17 의 요청, 같은 분에 밀린 고객사 C·D 의 요청, 입력이 계약 상한과 꼭 같은 8,000 토큰인 고객사 K 의 요청, SLO 를 넘겼지만 시간대 전체로는 1% 에 못 미치는 고객사 A 의 요청을 넣었습니다.
| request_id | tenant_id | endpoint_id | accel_pool | arrived_at | input_tokens | output_tokens | ttft_ms | tpot_ms |
|---|---|---|---|---|---|---|---|---|
| req-c03-100212-0091 | tnt-c03 | ep-c-chat-01 | gpu-pool-1 | 2026-10-13T10:02:12.004+09:00 | 1,040 | 240 | 262 | 31 |
| req-c03-101641-0733 | tnt-c03 | ep-c-chat-01 | gpu-pool-1 | 2026-10-13T10:16:41.870+09:00 | 960 | 188 | 1,910 | 64 |
| req-c03-101704-0758 | tnt-c03 | ep-c-chat-01 | gpu-pool-1 | 2026-10-13T10:17:04.213+09:00 | 1,180 | 212 | 2,340 | 71 |
| req-f06-100330-0012 | tnt-f06 | ep-f-batch-01 | gpu-pool-1 | 2026-10-13T10:03:30.512+09:00 | 5,880 | 500 | 1,120 | 48 |
| req-f06-101658-0688 | tnt-f06 | ep-f-batch-01 | gpu-pool-1 | 2026-10-13T10:16:58.102+09:00 | 9,420 | 512 | 6,880 | 138 |
아래 JSON Schema 를 그대로 만족하는 요청 로그를 엔드포인트 6개, 한 시간 분량(엔드포인트당 200~2,000줄)으로 더 만들어 주세요. {"type":"object","properties":{"request_id":{"type":"string","description":"부하 생성기가 요청마다 붙인 번호입니다"},"tenant_id":{"type":"string","description":"요청을 보낸 테넌트입니다. 부하 생성기의 가상 테넌트 이름입니다"},"endpoint_id":{"type":"string","description":"테넌트가 호출한 엔드포인트입니다"},"accel_pool":{"type":"string","enum":["gpu-pool-1","npu-pool-2"],"description":"요청을 처리한 가속기 풀입니다. gpu-pool-1 은 지원자가 띄운 vLLM 에서 잰 값이고 npu-pool-2 는 합성한 값입니다"},"arrived_at":{"type":"string","description":"요청을 보낸 시각입니다. ISO 8601, 밀리초, +09:00 입니다"},"input_tokens":{"type":"integer","minimum":1,"description":"응답 usage 의 prompt_tokens 입니다"},"output_tokens":{"type":"integer","minimum":1,"description":"응답 usage 의 completion_tokens 입니다"},"ttft_ms":{"type":"integer","minimum":1,"description":"보낸 시각부터 첫 스트리밍 청크까지 걸린 시간(ms)입니다"},"tpot_ms":{"type":"number","minimum":0,"description":"첫 청크부터 마지막 청크까지 걸린 시간을 output_tokens − 1 로 나눈 값(ms)입니다"}},"required":["request_id","tenant_id","endpoint_id","accel_pool","arrived_at","input_tokens","output_tokens","ttft_ms","tpot_ms"]} tenant_id·endpoint_id 는 함께 주는 tenant_contracts 의 짝을 쓰고, 같은 accel_pool 의 테넌트끼리는 시각이 겹치게 하세요. 평소에는 ttft_ms 200~900, tpot_ms 25~70 으로 두되, 같은 풀에 요청이 몰린 분에는 그 풀의 모든 테넌트 값이 함께 오르게 하세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 한 테넌트가 계약 동시성의 2~3배를 10~30분 동안 보내는 구간, (2) 계약 상한을 넘는 입력이 SLO 를 넘긴 요청의 절반 이상인 시간대와 입력이 상한과 꼭 같은 요청, (3) 계약 안에서 보냈는데 같은 풀의 다른 테넌트 때문에 ttft_ms 가 오른 시간대, (4) SLO 를 넘긴 요청이 그 시간대 요청 수의 꼭 1% 인 시간대. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도(arrived_at 이 '10:17:04' 처럼 날짜 없는 시각, output_tokens 가 0)는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"request_id":{"type":"string","description":"부하 생성기가 요청마다 붙인 번호입니다"},"tenant_id":{"type":"string","description":"요청을 보낸 테넌트입니다. 부하 생성기의 가상 테넌트 이름입니다"},"endpoint_id":{"type":"string","description":"테넌트가 호출한 엔드포인트입니다"},"accel_pool":{"type":"string","enum":["gpu-pool-1","npu-pool-2"],"description":"요청을 처리한 가속기 풀입니다. gpu-pool-1 은 지원자가 띄운 vLLM 에서 잰 값이고 npu-pool-2 는 합성한 값입니다"},"arrived_at":{"type":"string","description":"요청을 보낸 시각입니다. ISO 8601, 밀리초, +09:00 입니다"},"input_tokens":{"type":"integer","minimum":1,"description":"응답 usage 의 prompt_tokens 입니다"},"output_tokens":{"type":"integer","minimum":1,"description":"응답 usage 의 completion_tokens 입니다"},"ttft_ms":{"type":"integer","minimum":1,"description":"보낸 시각부터 첫 스트리밍 청크까지 걸린 시간(ms)입니다"},"tpot_ms":{"type":"number","minimum":0,"description":"첫 청크부터 마지막 청크까지 걸린 시간을 output_tokens − 1 로 나눈 값(ms)입니다"}},"required":["request_id","tenant_id","endpoint_id","accel_pool","arrived_at","input_tokens","output_tokens","ttft_ms","tpot_ms"]}Token Factory 테넌트가 엔드포인트마다 맺은 SLO 계약표입니다. TTFT·TPOT 목표와 동시 요청·입력 토큰 상한이 엔드포인트마다 다르고, 한 테넌트가 서로 다른 SLO 의 엔드포인트 둘을 가진 경우(고객사 C)와, 같은 풀에 SLO 가 빡빡한 대화형과 느슨한 배치형이 섞여 있는 경우를 넣었습니다.
| tenant_id | tenant_name | endpoint_id | model | ttft_slo_ms | tpot_slo_ms | max_concurrency | max_input_tokens |
|---|---|---|---|---|---|---|---|
| tnt-a01 | 고객사 A · 보험 상담 챗봇 | ep-a-chat-01 | qwen3-8b-fp8 | 800 | 60 | 8 | 4,000 |
| tnt-b02 | 고객사 B · 쇼핑몰 FAQ | ep-b-faq-01 | qwen3-8b-fp8 | 1,000 | 70 | 4 | 2,000 |
| tnt-c03 | 고객사 C · 통신 상담 챗봇 | ep-c-chat-01 | qwen3-8b-fp8 | 800 | 60 | 12 | 4,000 |
| tnt-c03 | 고객사 C · 상담 내용 요약 | ep-c-sum-02 | qwen3-8b-fp8 | 3,000 | 100 | 4 | 16,000 |
| tnt-d04 | 고객사 D · 사내 문서 검색 | ep-d-search-01 | qwen3-8b-fp8 | 1,500 | 80 | 8 | 6,000 |
아래 JSON Schema 를 그대로 만족하는 테넌트 계약을 테넌트 6곳·엔드포인트 10개 분량으로 더 만들어 주세요. {"type":"object","properties":{"tenant_id":{"type":"string"},"tenant_name":{"type":"string","description":"화면에 띄우는 테넌트 이름과 서비스입니다"},"endpoint_id":{"type":"string"},"model":{"type":"string","description":"엔드포인트가 쓰는 모델입니다"},"ttft_slo_ms":{"type":"integer","minimum":1,"description":"계약한 TTFT 목표(ms)입니다. 요청의 ttft_ms 가 이 값을 넘으면 SLO 를 넘긴 요청입니다"},"tpot_slo_ms":{"type":"integer","minimum":1,"description":"계약한 TPOT 목표(ms)입니다. 요청의 tpot_ms 가 이 값을 넘으면 SLO 를 넘긴 요청입니다"},"max_concurrency":{"type":"integer","minimum":1,"description":"그 엔드포인트로 동시에 보낼 수 있다고 계약한 요청 수입니다"},"max_input_tokens":{"type":"integer","minimum":1,"description":"계약한 요청당 입력 토큰 상한입니다"}},"required":["tenant_id","tenant_name","endpoint_id","model","ttft_slo_ms","tpot_slo_ms","max_concurrency","max_input_tokens"]} 대화형 엔드포인트는 ttft_slo_ms 500~1,000·tpot_slo_ms 40~70, 배치·문서형은 ttft_slo_ms 2,000~6,000·tpot_slo_ms 80~150 으로 두고, max_input_tokens 는 2,000~16,000 사이에서 고르세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 한 테넌트가 서로 다른 SLO 의 엔드포인트 둘을 가진 계약, (2) 같은 모델에 SLO 가 빡빡한 대화형과 느슨한 배치형이 함께 있는 계약. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도(ttft_slo_ms 가 '800ms' 같은 문자열)는 별도로 2건만 표시해 두세요.{"type":"object","properties":{"tenant_id":{"type":"string"},"tenant_name":{"type":"string","description":"화면에 띄우는 테넌트 이름과 서비스입니다"},"endpoint_id":{"type":"string"},"model":{"type":"string","description":"엔드포인트가 쓰는 모델입니다"},"ttft_slo_ms":{"type":"integer","minimum":1,"description":"계약한 TTFT 목표(ms)입니다. 요청의 ttft_ms 가 이 값을 넘으면 SLO 를 넘긴 요청입니다"},"tpot_slo_ms":{"type":"integer","minimum":1,"description":"계약한 TPOT 목표(ms)입니다. 요청의 tpot_ms 가 이 값을 넘으면 SLO 를 넘긴 요청입니다"},"max_concurrency":{"type":"integer","minimum":1,"description":"그 엔드포인트로 동시에 보낼 수 있다고 계약한 요청 수입니다"},"max_input_tokens":{"type":"integer","minimum":1,"description":"계약한 요청당 입력 토큰 상한입니다"}},"required":["tenant_id","tenant_name","endpoint_id","model","ttft_slo_ms","tpot_slo_ms","max_concurrency","max_input_tokens"]}가속기 풀의 분 단위 지표입니다. gpu-pool-1 은 vLLM /metrics 의 KV 캐시 사용률(vllm:kv_cache_usage_perc, 버전에 따라 vllm:gpu_cache_usage_perc)과 대기 요청 수(vllm:num_requests_waiting)를 1분마다 긁은 값이고, npu-pool-2 는 실장비 없이 합성한 값입니다. npu-pool-2 의 15:41 분은 지표 수집기가 응답하지 않은 분이라 두 값이 비어(null) 있고, 대기 요청이 0 인 한가한 분도 넣었습니다.
| accel_pool | minute | kv_cache_util | queued_requests |
|---|---|---|---|
| gpu-pool-1 | 2026-10-13T09:41+09:00 | 0.31 | 0 |
| gpu-pool-1 | 2026-10-13T10:02+09:00 | 0.38 | 0 |
| gpu-pool-1 | 2026-10-13T10:16+09:00 | 0.94 | 29 |
| gpu-pool-1 | 2026-10-13T10:17+09:00 | 0.96 | 37 |
| gpu-pool-1 | 2026-10-13T10:30+09:00 | 0.71 | 4 |
아래 JSON Schema 를 그대로 만족하는 풀 지표를 풀 2개, 한 시간 분량(풀당 60분)으로 더 만들어 주세요. {"type":"object","properties":{"accel_pool":{"type":"string","enum":["gpu-pool-1","npu-pool-2"]},"minute":{"type":"string","description":"지표를 긁은 분입니다(YYYY-MM-DDTHH:MM+09:00)"},"kv_cache_util":{"type":["number","null"],"minimum":0,"maximum":1,"description":"그 분의 KV 캐시 사용률(0~1)입니다. gpu-pool-1 은 vLLM /metrics 의 vllm:kv_cache_usage_perc 입니다. 수집기가 응답하지 않은 분은 null 입니다"},"queued_requests":{"type":["integer","null"],"minimum":0,"description":"그 분에 대기열에서 기다리던 요청 수입니다. gpu-pool-1 은 vllm:num_requests_waiting 입니다. 수집기가 응답하지 않은 분은 null 입니다"}},"required":["accel_pool","minute","kv_cache_util","queued_requests"]} 함께 주는 요청 로그에서 요청이 몰린 분에는 kv_cache_util 과 queued_requests 가 함께 오르게 하세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 수집기가 응답하지 않아 kv_cache_util·queued_requests 가 둘 다 null 인 분, (2) kv_cache_util 이 0.95 를 넘고 대기 요청이 30건을 넘는 분, (3) 대기 요청이 0 인 한가한 분. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도(kv_cache_util 이 96 처럼 백분율 숫자)는 별도로 2건만 표시해 두세요.{"type":"object","properties":{"accel_pool":{"type":"string","enum":["gpu-pool-1","npu-pool-2"]},"minute":{"type":"string","description":"지표를 긁은 분입니다(YYYY-MM-DDTHH:MM+09:00)"},"kv_cache_util":{"type":["number","null"],"minimum":0,"maximum":1,"description":"그 분의 KV 캐시 사용률(0~1)입니다. gpu-pool-1 은 vLLM /metrics 의 vllm:kv_cache_usage_perc 입니다. 수집기가 응답하지 않은 분은 null 입니다"},"queued_requests":{"type":["integer","null"],"minimum":0,"description":"그 분에 대기열에서 기다리던 요청 수입니다. gpu-pool-1 은 vllm:num_requests_waiting 입니다. 수집기가 응답하지 않은 분은 null 입니다"}},"required":["accel_pool","minute","kv_cache_util","queued_requests"]}| Python | asyncio 부하 생성기(OpenAI 호환 스트리밍 클라이언트로 TTFT·TPOT 기록), 분 단위 동시 요청 수를 세는 sweep line 계산과 네 판정 엔진, 판정 결과를 돌려주는 FastAPI API 를 씁니다. |
| Go | 2주차에 vLLM 앞에 서는 게이트웨이를 만듭니다. API 키로 테넌트를 가리고, 테넌트별 세마포어로 계약 동시성을 넘는 요청을 대기열에 세우며, 게이트웨이 쪽에서 TTFT·TPOT 를 다시 잽니다. |
| vLLM | L4 24GB 한 장에 Qwen3-8B-FP8 을 올려 가상 테넌트 여섯의 요청을 한 엔진에서 받게 하고, /metrics 의 KV 캐시 사용률과 대기 요청 수를 1분마다 긁어 풀 지표로 씁니다. |
| KV Cache · Continuous Batching | 긴 입력과 많은 동시 요청이 KV 캐시를 채우면 continuous batching 이 새 요청을 대기열에 두는 동작을 부하로 재현하고, 그 분의 KV 캐시 사용률과 대기 요청 수를 성능 격리 실패의 근거로 보여줍니다. |
| FP8 양자화 | Qwen3-8B 의 FP8 체크포인트로 가중치를 줄여 L4 에 KV 캐시 자리를 남기고, 그 자리가 차기 시작하는 동시 요청 수를 부하로 찾습니다. |
| TTFT · TPOT · Goodput | TTFT 는 요청을 보낸 시각부터 첫 스트리밍 청크까지, TPOT 는 첫 청크부터 마지막 청크까지를 출력 토큰 수 − 1 로 나눈 값으로 잽니다. 3주차 회귀 표에 테넌트별 Goodput 을 냅니다. |
| Kubernetes | 3주차에 GPU VM 한 대의 k3s 에 NVIDIA device plugin 을 깔고 vLLM·게이트웨이·Prometheus 를 Helm 차트로 올립니다. 같은 부하 시나리오를 밤마다 다시 돌리는 CronJob 도 여기 둡니다. |
| 추론 벤치마크·회귀 테스트 자동화 | vLLM 설정(--max-num-seqs, --max-num-batched-tokens, --scheduling-policy priority)을 바꿀 때마다 같은 시나리오를 돌려 판정이 바뀐 시간대와 Goodput 을 설정별로 견주는 회귀 표를 만듭니다. |
| React | 판정별 시간대 목록과, 펼치면 열리는 분 단위 시간축(위반 수 막대·동시 요청 수와 계약 선·KV 캐시·대기열)을 그립니다. |
✗ fail → 고객사 D 가 걸리면 1% 비교를 반올림한 비율이 아니라 정수 비교로 고칩니다.
✓ pass → 여기서 멈춰도 완결입니다. 다음에는 Go 게이트웨이를 vLLM 앞에 세우고, 원인을 심은 48개 시간대를 상한 없이 한 번, 상한을 걸고 한 번 돌립니다.
✓ pass → 여기까지도 완결입니다. 다음에는 GPU VM 에 k3s 를 깔아 vLLM·게이트웨이·Prometheus 를 올리고, 같은 시나리오를 밤마다 다시 돌리는 CronJob 을 겁니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · window_key + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
SLO 안에서 처리됨SLO 안계약 동시성을 넘겨 보냄동시성 계약 초과계약보다 긴 입력이 몰림긴 입력 몰림계약 안인데 밀림 · 성능 격리 실패성능 격리 실패테넌트·엔드포인트의 한 시간 시간대 하나에 SLO 위반 원인 판정 하나와 그 근거(위반 수와 비율, 분 단위 동시 요청 수, 긴 입력 수, 성능 격리 실패일 때 위반이 몰린 분의 풀 지표와 다른 테넌트 요청 배율)를 남기는 형식입니다.
{"type":"object","properties":{"window_key":{"type":"string","description":"endpoint_id 와 시간대 시작(YYYY-MM-DDTHH)을 / 로 이은 화면 행 키입니다"},"tenant_id":{"type":"string"},"endpoint_id":{"type":"string"},"accel_pool":{"type":"string","enum":["gpu-pool-1","npu-pool-2"]},"window_start":{"type":"string","description":"시간대 시작 시각입니다. arrived_at 기준 정시부터 한 시간입니다"},"verdict":{"type":"string","enum":["SLO 안에서 처리됨","계약 동시성을 넘겨 보냄","계약보다 긴 입력이 몰림","계약 안인데 밀림 · 성능 격리 실패"],"description":"SLO 를 넘긴 요청이 1% 이하이면 SLO 안에서 처리됨이고, 넘으면 계약 동시성을 넘겨 보냄, 계약보다 긴 입력이 몰림 순으로 앞선 하나이며, 둘 다 아니면 계약 안인데 밀림 · 성능 격리 실패입니다"},"also_found":{"type":"array","items":{"type":"string","enum":["계약보다 긴 입력이 몰림"]},"description":"동시성 사유로 판정됐는데 긴 입력 사유도 성립하면 담습니다"},"request_count":{"type":"integer","minimum":0,"description":"정적 계산값입니다. 그 시간대에 arrived_at 이 든 그 tenant_id·endpoint_id 요청 수입니다"},"slo_miss_count":{"type":"integer","minimum":0,"description":"정적 계산값입니다. ttft_ms 가 ttft_slo_ms 를, 또는 tpot_ms 가 tpot_slo_ms 를 넘은 요청 수입니다"},"slo_miss_pct":{"type":"number","minimum":0,"maximum":100,"description":"정적 계산값입니다. slo_miss_count ÷ request_count × 100 을 소수 첫째 자리로 반올림한 화면 표시값이며 1% 비교에는 쓰지 않습니다"},"peak_concurrency":{"type":"integer","minimum":0,"description":"정적 계산값입니다. 같은 tenant_id·endpoint_id 요청마다 arrived_at 부터 ttft_ms + tpot_ms × output_tokens 동안의 구간을 만들어, 그 시간대 안에서 가장 많이 겹친 수입니다"},"max_concurrency":{"type":"integer","minimum":1,"description":"tenant_contracts 의 계약 동시성입니다"},"over_concurrency_minutes":{"type":"integer","minimum":0,"description":"정적 계산값입니다. 분 안의 동시 요청 수 최댓값이 max_concurrency 를 넘은 분의 수입니다"},"misses_in_over_concurrency":{"type":"integer","minimum":0,"description":"정적 계산값입니다. SLO 를 넘긴 요청 가운데 arrived_at 이 동시성을 넘은 분에 든 요청 수입니다"},"misses_with_long_input":{"type":"integer","minimum":0,"description":"정적 계산값입니다. SLO 를 넘긴 요청 가운데 input_tokens 가 max_input_tokens 를 넘은 요청 수입니다"},"worst_minute":{"type":["string","null"],"description":"SLO 를 넘긴 요청이 가장 많이 든 분입니다. 같으면 이른 분이고, SLO 안에서 처리됨이면 null 입니다"},"worst_minute_misses":{"type":["integer","null"],"minimum":0,"description":"worst_minute 에 든 SLO 를 넘긴 요청 수입니다"},"kv_cache_util_at_worst":{"type":["number","null"],"minimum":0,"maximum":1,"description":"성능 격리 실패일 때만 채웁니다. 같은 accel_pool 의 worst_minute pool_metrics.kv_cache_util 이고, 그 분 값이 비면 null 입니다"},"kv_cache_util_prev_hour_avg":{"type":["number","null"],"minimum":0,"maximum":1,"description":"성능 격리 실패일 때만 채웁니다. worst_minute 앞 60분 kv_cache_util 의 평균이고, 값이 있는 분만 씁니다"},"queued_at_worst":{"type":["integer","null"],"minimum":0,"description":"성능 격리 실패일 때만 채웁니다. 같은 accel_pool 의 worst_minute pool_metrics.queued_requests 이고, 그 분 값이 비면 null 입니다"},"queued_prev_hour_avg":{"type":["number","null"],"minimum":0,"description":"성능 격리 실패일 때만 채웁니다. worst_minute 앞 60분 queued_requests 의 평균입니다"},"other_tenant_requests_at_worst":{"type":["integer","null"],"minimum":0,"description":"성능 격리 실패일 때만 채웁니다. worst_minute 에 같은 accel_pool 로 들어온 다른 tenant_id 요청 수입니다"},"other_tenant_requests_prev_hour_avg":{"type":["number","null"],"minimum":0,"description":"성능 격리 실패일 때만 채웁니다. worst_minute 앞 60분 동안 같은 accel_pool 로 들어온 다른 tenant_id 요청 수 ÷ 60 입니다"},"other_tenant_requests_ratio":{"type":["number","null"],"minimum":0,"description":"정적 계산값입니다. 성능 격리 실패일 때만 채웁니다. other_tenant_requests_at_worst ÷ other_tenant_requests_prev_hour_avg 를 소수 첫째 자리로 반올림합니다"},"evidence":{"type":"array","items":{"type":"object","properties":{"table":{"type":"string","enum":["request_logs","tenant_contracts","pool_metrics"]},"row":{"type":["string","null"],"description":"그 표의 키입니다. request_logs 는 request_id, tenant_contracts 는 tenant_id/endpoint_id, pool_metrics 는 accel_pool/minute 입니다. agg 항목이면 null 입니다"},"field":{"type":"string"},"value":{"type":["string","integer","number","null"]},"agg":{"type":"string","enum":["count","sum","avg","min","max"]},"filter":{"type":"string","description":"agg 항목이 센 행의 조건입니다"}},"required":["table","row","field","value"]}},"notes":{"type":"array","items":{"type":"string"},"description":"판정에 들지 않았지만 기술 지원 담당자가 볼 사실입니다. 다른 테넌트의 tenant_id·tenant_name 은 쓰지 않습니다"},"summary":{"type":"string"}},"required":["window_key","tenant_id","endpoint_id","accel_pool","window_start","verdict","also_found","request_count","slo_miss_count","slo_miss_pct","peak_concurrency","max_concurrency","over_concurrency_minutes","misses_in_over_concurrency","misses_with_long_input","worst_minute","worst_minute_misses","kv_cache_util_at_worst","kv_cache_util_prev_hour_avg","queued_at_worst","queued_prev_hour_avg","other_tenant_requests_at_worst","other_tenant_requests_prev_hour_avg","other_tenant_requests_ratio","evidence","notes","summary"]}[
{
"window_key": "ep-c-chat-01/2026-10-13T10",
"tenant_id": "tnt-c03",
"endpoint_id": "ep-c-chat-01",
"accel_pool": "gpu-pool-1",
"window_start": "2026-10-13T10:00+09:00",
"verdict": "계약 안인데 밀림 · 성능 격리 실패",
"also_found": [],
"request_count": 1840,
"slo_miss_count": 86,
"slo_miss_pct": 4.7,
"peak_concurrency": 9,
"max_concurrency": 12,
"over_concurrency_minutes": 0,
"misses_in_over_concurrency": 0,
"misses_with_long_input": 2,
"worst_minute": "2026-10-13T10:17+09:00",
"worst_minute_misses": 21,
"kv_cache_util_at_worst": 0.96,
"kv_cache_util_prev_hour_avg": 0.47,
"queued_at_worst": 37,
"queued_prev_hour_avg": 4.9,
"other_tenant_requests_at_worst": 91,
"other_tenant_requests_prev_hour_avg": 36.6,
"other_tenant_requests_ratio": 2.5,
"evidence": [
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 1840,
"agg": "count",
"filter": "tnt-c03·ep-c-chat-01 · 10:00~11:00 도착"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 86,
"agg": "count",
"filter": "위 요청 중 ttft_ms > 800 또는 tpot_ms > 60"
},
{
"table": "request_logs",
"row": "req-c03-101704-0758",
"field": "ttft_ms",
"value": 2340
},
{
"table": "tenant_contracts",
"row": "tnt-c03/ep-c-chat-01",
"field": "ttft_slo_ms",
"value": 800
},
{
"table": "request_logs",
"row": "req-c03-101704-0758",
"field": "tpot_ms",
"value": 71
},
{
"table": "tenant_contracts",
"row": "tnt-c03/ep-c-chat-01",
"field": "tpot_slo_ms",
"value": 60
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 9,
"agg": "count",
"filter": "10:17:12 에 진행 중이던 tnt-c03·ep-c-chat-01 요청(이 시간대 최대)"
},
{
"table": "tenant_contracts",
"row": "tnt-c03/ep-c-chat-01",
"field": "max_concurrency",
"value": 12
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 0,
"agg": "count",
"filter": "SLO 를 넘긴 86건 중 동시 요청 수가 12 를 넘은 분에 도착"
},
{
"table": "request_logs",
"row": null,
"field": "input_tokens",
"value": 2,
"agg": "count",
"filter": "SLO 를 넘긴 86건 중 input_tokens > 4,000"
},
{
"table": "tenant_contracts",
"row": "tnt-c03/ep-c-chat-01",
"field": "max_input_tokens",
"value": 4000
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 21,
"agg": "count",
"filter": "10:17 분 도착 · SLO 를 넘긴 tnt-c03·ep-c-chat-01 요청"
},
{
"table": "pool_metrics",
"row": "gpu-pool-1/2026-10-13T10:17+09:00",
"field": "kv_cache_util",
"value": 0.96
},
{
"table": "pool_metrics",
"row": null,
"field": "kv_cache_util",
"value": 0.47,
"agg": "avg",
"filter": "gpu-pool-1 · 09:17~10:16"
},
{
"table": "pool_metrics",
"row": "gpu-pool-1/2026-10-13T10:17+09:00",
"field": "queued_requests",
"value": 37
},
{
"table": "pool_metrics",
"row": null,
"field": "queued_requests",
"value": 4.9,
"agg": "avg",
"filter": "gpu-pool-1 · 09:17~10:16"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 91,
"agg": "count",
"filter": "gpu-pool-1 · 10:17 분 도착 · tnt-c03 이 아닌 tenant_id"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 2196,
"agg": "count",
"filter": "gpu-pool-1 · 09:17~10:16 도착 · tnt-c03 이 아닌 tenant_id"
}
],
"notes": [
"SLO 를 넘긴 86건 가운데 동시 요청 수가 계약 12 를 넘은 분에 든 요청은 없고, 입력이 4,000 토큰을 넘은 요청은 2건(2.3%)이라 고객 쪽 두 사유 모두 절반에 못 미칩니다.",
"10:17 분 같은 풀의 다른 테넌트 요청은 91건으로 직전 1시간 분당 평균 36.6건의 2.5배이고, KV 캐시 사용률은 평균 0.47 에서 0.96 으로, 대기 요청은 평균 4.9건에서 37건으로 올랐습니다.",
"req-c03-101704-0758 은 입력 1,180 토큰으로 계약 안이었는데 첫 토큰까지 2,340ms 가 걸렸습니다."
],
"summary": "계약 동시성과 입력 상한 안에서 보냈는데 10:17 전후 같은 풀에 다른 테넌트 요청이 몰리면서 SLO 를 넘긴 요청이 4.7% 나왔습니다."
},
{
"window_key": "ep-f-batch-01/2026-10-13T10",
"tenant_id": "tnt-f06",
"endpoint_id": "ep-f-batch-01",
"accel_pool": "gpu-pool-1",
"window_start": "2026-10-13T10:00+09:00",
"verdict": "계약 동시성을 넘겨 보냄",
"also_found": [
"계약보다 긴 입력이 몰림"
],
"request_count": 1320,
"slo_miss_count": 412,
"slo_miss_pct": 31.2,
"peak_concurrency": 48,
"max_concurrency": 16,
"over_concurrency_minutes": 29,
"misses_in_over_concurrency": 363,
"misses_with_long_input": 227,
"worst_minute": "2026-10-13T10:17+09:00",
"worst_minute_misses": 41,
"kv_cache_util_at_worst": null,
"kv_cache_util_prev_hour_avg": null,
"queued_at_worst": null,
"queued_prev_hour_avg": null,
"other_tenant_requests_at_worst": null,
"other_tenant_requests_prev_hour_avg": null,
"other_tenant_requests_ratio": null,
"evidence": [
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 1320,
"agg": "count",
"filter": "tnt-f06·ep-f-batch-01 · 10:00~11:00 도착"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 412,
"agg": "count",
"filter": "위 요청 중 ttft_ms > 5,000 또는 tpot_ms > 120"
},
{
"table": "request_logs",
"row": "req-f06-101658-0688",
"field": "ttft_ms",
"value": 6880
},
{
"table": "tenant_contracts",
"row": "tnt-f06/ep-f-batch-01",
"field": "ttft_slo_ms",
"value": 5000
},
{
"table": "request_logs",
"row": "req-f06-101658-0688",
"field": "tpot_ms",
"value": 138
},
{
"table": "tenant_contracts",
"row": "tnt-f06/ep-f-batch-01",
"field": "tpot_slo_ms",
"value": 120
},
{
"table": "request_logs",
"row": "req-f06-101658-0688",
"field": "output_tokens",
"value": 512
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 48,
"agg": "count",
"filter": "10:16:58 에 진행 중이던 tnt-f06·ep-f-batch-01 요청(이 시간대 최대)"
},
{
"table": "tenant_contracts",
"row": "tnt-f06/ep-f-batch-01",
"field": "max_concurrency",
"value": 16
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 16,
"agg": "count",
"filter": "10:05 분 안에서 진행 중이던 tnt-f06·ep-f-batch-01 요청의 최댓값"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 363,
"agg": "count",
"filter": "SLO 를 넘긴 412건 중 동시 요청 수가 16 을 넘은 분(29분)에 도착"
},
{
"table": "request_logs",
"row": "req-f06-101658-0688",
"field": "input_tokens",
"value": 9420
},
{
"table": "tenant_contracts",
"row": "tnt-f06/ep-f-batch-01",
"field": "max_input_tokens",
"value": 8000
},
{
"table": "request_logs",
"row": null,
"field": "input_tokens",
"value": 227,
"agg": "count",
"filter": "SLO 를 넘긴 412건 중 input_tokens > 8,000"
}
],
"notes": [
"SLO 를 넘긴 412건 중 363건(88.1%)이 동시 요청 수가 계약 16 을 넘은 29분 안에 들어왔습니다. 입력이 8,000 토큰을 넘은 요청도 227건(55.1%)이라 긴 입력 사유가 함께 성립합니다.",
"10:05 분의 동시 요청 수 최댓값은 16 으로 계약과 같아 넘은 분에 세지 않았습니다.",
"req-f06-101658-0688 은 6,880 + 138 × 512 = 77,536ms 동안 동시 요청 수에 들어갑니다.",
"같은 시각 같은 풀의 다른 테넌트 시간대 하나가 성능 격리 실패로 걸렸습니다."
],
"summary": "계약 동시성 16 을 넘겨 최대 48개를 동시에 보낸 29분 동안 SLO 를 넘긴 요청이 몰렸습니다."
},
{
"window_key": "ep-d-search-01/2026-10-13T10",
"tenant_id": "tnt-d04",
"endpoint_id": "ep-d-search-01",
"accel_pool": "gpu-pool-1",
"window_start": "2026-10-13T10:00+09:00",
"verdict": "SLO 안에서 처리됨",
"also_found": [],
"request_count": 400,
"slo_miss_count": 4,
"slo_miss_pct": 1,
"peak_concurrency": 5,
"max_concurrency": 8,
"over_concurrency_minutes": 0,
"misses_in_over_concurrency": 0,
"misses_with_long_input": 0,
"worst_minute": null,
"worst_minute_misses": null,
"kv_cache_util_at_worst": null,
"kv_cache_util_prev_hour_avg": null,
"queued_at_worst": null,
"queued_prev_hour_avg": null,
"other_tenant_requests_at_worst": null,
"other_tenant_requests_prev_hour_avg": null,
"other_tenant_requests_ratio": null,
"evidence": [
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 400,
"agg": "count",
"filter": "tnt-d04·ep-d-search-01 · 10:00~11:00 도착"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 4,
"agg": "count",
"filter": "위 요청 중 ttft_ms > 1,500 또는 tpot_ms > 80"
},
{
"table": "request_logs",
"row": "req-d04-101710-0151",
"field": "ttft_ms",
"value": 1720
},
{
"table": "tenant_contracts",
"row": "tnt-d04/ep-d-search-01",
"field": "ttft_slo_ms",
"value": 1500
},
{
"table": "request_logs",
"row": "req-d04-103005-0262",
"field": "ttft_ms",
"value": 240
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 5,
"agg": "count",
"filter": "이 시간대 tnt-d04·ep-d-search-01 동시 요청 수 최대"
},
{
"table": "tenant_contracts",
"row": "tnt-d04/ep-d-search-01",
"field": "max_concurrency",
"value": 8
}
],
"notes": [
"SLO 를 넘긴 요청이 400건 중 4건이라 4 × 100 = 400 이 요청 수를 넘지 않습니다.",
"4건 모두 10:16~10:18 에 들어왔고, 같은 시각 같은 풀의 다른 테넌트 시간대가 성능 격리 실패로 걸렸습니다."
],
"summary": "SLO 를 넘긴 요청이 꼭 1% 라서 1% 를 넘지 않아 SLO 안에서 처리됨으로 판정했습니다."
},
{
"window_key": "ep-a-chat-01/2026-10-13T09",
"tenant_id": "tnt-a01",
"endpoint_id": "ep-a-chat-01",
"accel_pool": "gpu-pool-1",
"window_start": "2026-10-13T09:00+09:00",
"verdict": "SLO 안에서 처리됨",
"also_found": [],
"request_count": 1210,
"slo_miss_count": 2,
"slo_miss_pct": 0.2,
"peak_concurrency": 6,
"max_concurrency": 8,
"over_concurrency_minutes": 0,
"misses_in_over_concurrency": 0,
"misses_with_long_input": 0,
"worst_minute": null,
"worst_minute_misses": null,
"kv_cache_util_at_worst": null,
"kv_cache_util_prev_hour_avg": null,
"queued_at_worst": null,
"queued_prev_hour_avg": null,
"other_tenant_requests_at_worst": null,
"other_tenant_requests_prev_hour_avg": null,
"other_tenant_requests_ratio": null,
"evidence": [
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 1210,
"agg": "count",
"filter": "tnt-a01·ep-a-chat-01 · 09:00~10:00 도착"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 2,
"agg": "count",
"filter": "위 요청 중 ttft_ms > 800 또는 tpot_ms > 60"
},
{
"table": "request_logs",
"row": "req-a01-095233-1027",
"field": "ttft_ms",
"value": 830
},
{
"table": "tenant_contracts",
"row": "tnt-a01/ep-a-chat-01",
"field": "ttft_slo_ms",
"value": 800
},
{
"table": "request_logs",
"row": "req-a01-095233-1027",
"field": "input_tokens",
"value": 3960
},
{
"table": "tenant_contracts",
"row": "tnt-a01/ep-a-chat-01",
"field": "max_input_tokens",
"value": 4000
},
{
"table": "request_logs",
"row": "req-a01-094120-0884",
"field": "ttft_ms",
"value": 214
}
],
"notes": [
"09:52 의 req-a01-095233-1027 은 입력 3,960 토큰으로 TTFT 830ms 가 SLO 800ms 를 넘었지만 시간대 판정은 요청 하나가 아니라 한 시간 비율로 합니다."
],
"summary": "한 시간 1,210건 가운데 SLO 를 넘긴 요청은 2건뿐입니다."
},
{
"window_key": "ep-k-doc-01/2026-10-13T09",
"tenant_id": "tnt-k11",
"endpoint_id": "ep-k-doc-01",
"accel_pool": "npu-pool-2",
"window_start": "2026-10-13T09:00+09:00",
"verdict": "계약보다 긴 입력이 몰림",
"also_found": [],
"request_count": 640,
"slo_miss_count": 54,
"slo_miss_pct": 8.4,
"peak_concurrency": 19,
"max_concurrency": 24,
"over_concurrency_minutes": 0,
"misses_in_over_concurrency": 0,
"misses_with_long_input": 33,
"worst_minute": "2026-10-13T09:22+09:00",
"worst_minute_misses": 7,
"kv_cache_util_at_worst": null,
"kv_cache_util_prev_hour_avg": null,
"queued_at_worst": null,
"queued_prev_hour_avg": null,
"other_tenant_requests_at_worst": null,
"other_tenant_requests_prev_hour_avg": null,
"other_tenant_requests_ratio": null,
"evidence": [
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 640,
"agg": "count",
"filter": "tnt-k11·ep-k-doc-01 · 09:00~10:00 도착"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 54,
"agg": "count",
"filter": "위 요청 중 ttft_ms > 2,000 또는 tpot_ms > 100"
},
{
"table": "request_logs",
"row": "req-k11-092208-0233",
"field": "input_tokens",
"value": 18400
},
{
"table": "request_logs",
"row": "req-k11-092208-0233",
"field": "ttft_ms",
"value": 3920
},
{
"table": "tenant_contracts",
"row": "tnt-k11/ep-k-doc-01",
"field": "ttft_slo_ms",
"value": 2000
},
{
"table": "request_logs",
"row": "req-k11-092231-0237",
"field": "tpot_ms",
"value": 104
},
{
"table": "tenant_contracts",
"row": "tnt-k11/ep-k-doc-01",
"field": "tpot_slo_ms",
"value": 100
},
{
"table": "tenant_contracts",
"row": "tnt-k11/ep-k-doc-01",
"field": "max_input_tokens",
"value": 8000
},
{
"table": "request_logs",
"row": null,
"field": "input_tokens",
"value": 33,
"agg": "count",
"filter": "SLO 를 넘긴 54건 중 input_tokens > 8,000"
},
{
"table": "request_logs",
"row": "req-k11-094012-0398",
"field": "input_tokens",
"value": 8000
},
{
"table": "request_logs",
"row": "req-k11-094012-0398",
"field": "ttft_ms",
"value": 2180
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 19,
"agg": "count",
"filter": "이 시간대 tnt-k11·ep-k-doc-01 동시 요청 수 최대"
},
{
"table": "tenant_contracts",
"row": "tnt-k11/ep-k-doc-01",
"field": "max_concurrency",
"value": 24
}
],
"notes": [
"SLO 를 넘긴 54건 중 33건(61.1%)이 입력 8,000 토큰을 넘었고, 동시 요청 수는 최대 19 로 계약 24 안입니다.",
"req-k11-094012-0398 은 TTFT 2,180ms 로 SLO 를 넘겼지만 입력이 계약 상한과 같은 8,000 토큰이라 긴 입력에 세지 않았습니다.",
"npu-pool-2 의 요청 로그와 지표는 실장비 없이 합성한 값입니다."
],
"summary": "계약 상한 8,000 토큰을 넘는 입력이 SLO 를 넘긴 요청의 61.1% 를 차지합니다."
},
{
"window_key": "ep-m-agent-01/2026-10-13T15",
"tenant_id": "tnt-m13",
"endpoint_id": "ep-m-agent-01",
"accel_pool": "npu-pool-2",
"window_start": "2026-10-13T15:00+09:00",
"verdict": "계약 안인데 밀림 · 성능 격리 실패",
"also_found": [],
"request_count": 880,
"slo_miss_count": 23,
"slo_miss_pct": 2.6,
"peak_concurrency": 11,
"max_concurrency": 16,
"over_concurrency_minutes": 0,
"misses_in_over_concurrency": 0,
"misses_with_long_input": 1,
"worst_minute": "2026-10-13T15:41+09:00",
"worst_minute_misses": 9,
"kv_cache_util_at_worst": null,
"kv_cache_util_prev_hour_avg": 0.44,
"queued_at_worst": null,
"queued_prev_hour_avg": 1.9,
"other_tenant_requests_at_worst": 58,
"other_tenant_requests_prev_hour_avg": 19.3,
"other_tenant_requests_ratio": 3,
"evidence": [
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 880,
"agg": "count",
"filter": "tnt-m13·ep-m-agent-01 · 15:00~16:00 도착"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 23,
"agg": "count",
"filter": "위 요청 중 ttft_ms > 1,000 또는 tpot_ms > 70"
},
{
"table": "request_logs",
"row": "req-m13-154107-0516",
"field": "ttft_ms",
"value": 1860
},
{
"table": "tenant_contracts",
"row": "tnt-m13/ep-m-agent-01",
"field": "ttft_slo_ms",
"value": 1000
},
{
"table": "request_logs",
"row": "req-m13-154107-0516",
"field": "tpot_ms",
"value": 88
},
{
"table": "tenant_contracts",
"row": "tnt-m13/ep-m-agent-01",
"field": "tpot_slo_ms",
"value": 70
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 11,
"agg": "count",
"filter": "이 시간대 tnt-m13·ep-m-agent-01 동시 요청 수 최대"
},
{
"table": "tenant_contracts",
"row": "tnt-m13/ep-m-agent-01",
"field": "max_concurrency",
"value": 16
},
{
"table": "request_logs",
"row": null,
"field": "input_tokens",
"value": 1,
"agg": "count",
"filter": "SLO 를 넘긴 23건 중 input_tokens > 6,000"
},
{
"table": "tenant_contracts",
"row": "tnt-m13/ep-m-agent-01",
"field": "max_input_tokens",
"value": 6000
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 9,
"agg": "count",
"filter": "15:41 분 도착 · SLO 를 넘긴 tnt-m13·ep-m-agent-01 요청"
},
{
"table": "pool_metrics",
"row": "npu-pool-2/2026-10-13T15:41+09:00",
"field": "kv_cache_util",
"value": null
},
{
"table": "pool_metrics",
"row": "npu-pool-2/2026-10-13T15:41+09:00",
"field": "queued_requests",
"value": null
},
{
"table": "pool_metrics",
"row": "npu-pool-2/2026-10-13T15:40+09:00",
"field": "kv_cache_util",
"value": 0.88
},
{
"table": "pool_metrics",
"row": "npu-pool-2/2026-10-13T15:42+09:00",
"field": "kv_cache_util",
"value": 0.9
},
{
"table": "pool_metrics",
"row": null,
"field": "kv_cache_util",
"value": 0.44,
"agg": "avg",
"filter": "npu-pool-2 · 14:41~15:40"
},
{
"table": "pool_metrics",
"row": null,
"field": "queued_requests",
"value": 1.9,
"agg": "avg",
"filter": "npu-pool-2 · 14:41~15:40"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 58,
"agg": "count",
"filter": "npu-pool-2 · 15:41 분 도착 · tnt-m13 이 아닌 tenant_id"
},
{
"table": "request_logs",
"row": null,
"field": "request_id",
"value": 1158,
"agg": "count",
"filter": "npu-pool-2 · 14:41~15:40 도착 · tnt-m13 이 아닌 tenant_id"
}
],
"notes": [
"15:41 분 npu-pool-2 지표가 비어 있어 그 분의 KV 캐시 사용률과 대기 요청 수는 비워 둡니다. 앞뒤 분은 15:40 에 0.88·9건, 15:42 에 0.90·11건입니다.",
"같은 분 같은 풀의 다른 테넌트 요청은 58건으로 직전 1시간 분당 평균 19.3건의 3.0배입니다.",
"npu-pool-2 의 요청 로그와 지표는 실장비 없이 합성한 값입니다."
],
"summary": "계약 안에서 보냈는데 15:41 전후 같은 풀의 다른 테넌트 요청이 세 배로 늘면서 SLO 를 넘긴 요청이 2.6% 나왔습니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 SLO 안에서 처리됨·계약 동시성을 넘겨 보냄·계약보다 긴 입력이 몰림·계약 안인데 밀림 · 성능 격리 실패 네 값 중 하나인지, also_found 가 계약보다 긴 입력이 몰림 하나만 담는지 확인합니다. 2. 필수 필드 window_key·tenant_id·endpoint_id·accel_pool·window_start·verdict·also_found·request_count·slo_miss_count·slo_miss_pct·peak_concurrency·max_concurrency·over_concurrency_minutes·misses_in_over_concurrency·misses_with_long_input·worst_minute·worst_minute_misses·kv_cache_util_at_worst·kv_cache_util_prev_hour_avg·queued_at_worst·queued_prev_hour_avg·other_tenant_requests_at_worst·other_tenant_requests_prev_hour_avg·other_tenant_requests_ratio·evidence·notes·summary 가 모두 존재하는지 확인합니다. 3. slo_miss_pct 가 0~100, kv_cache_util 계열이 0~1 안에 있고, slo_miss_count 가 request_count 이하, misses_in_over_concurrency·misses_with_long_input 이 slo_miss_count 이하인지 확인합니다. 4. 요청마다 ttft_ms 가 ttft_slo_ms 를, 또는 tpot_ms 가 tpot_slo_ms 를 넘으면 SLO 를 넘긴 요청으로 세어 slo_miss_count 를 다시 계산하고, slo_miss_pct 가 slo_miss_count ÷ request_count × 100 인지 다시 계산합니다. 고객사 C 는 86 ÷ 1,840 × 100 = 4.7 입니다. 5. slo_miss_count × 100 이 request_count 이하이면 verdict 는 SLO 안에서 처리됨입니다. 고객사 D 는 4 × 100 = 400 이 요청 수 400 과 같아 SLO 안에서 처리됨이고, 고객사 A 는 2 × 100 = 200 이 1,210 보다 작아 SLO 안에서 처리됨입니다. 6. 같은 tenant_id·endpoint_id 요청마다 arrived_at 부터 ttft_ms + tpot_ms × output_tokens 동안의 구간을 만들어 겹친 수를 다시 세고, 분 안의 최댓값이 max_concurrency 를 넘으면 그 분을 동시성을 넘은 분으로 둡니다. req-f06-101658-0688 의 구간은 6,880 + 138 × 512 = 77,536ms 이고, 고객사 F 의 10:05 분은 최댓값 16 이 계약 16 과 같아 넘은 분에 들지 않습니다. 7. misses_in_over_concurrency × 2 가 slo_miss_count 이상이면 verdict 는 계약 동시성을 넘겨 보냄이고, 다른 사유보다 먼저 판정합니다. 고객사 F 는 363 × 2 = 726 이 412 이상이라 이 판정이고, 고객사 C 는 0 이라 이 판정이 아닙니다. 8. 동시성 사유가 아니고 misses_with_long_input × 2 가 slo_miss_count 이상이면 verdict 는 계약보다 긴 입력이 몰림입니다. 고객사 K 는 33 × 2 = 66 이 54 이상이라 이 판정이고, 입력이 상한과 같은 8,000 토큰인 req-k11-094012-0398 은 세지 않습니다. 동시성 사유로 판정된 고객사 F 는 227 × 2 = 454 가 412 이상이라 also_found 에 긴 입력이 들어갑니다. 9. SLO 를 넘긴 요청이 1% 를 넘는데 7·8번에 해당하지 않으면 verdict 는 계약 안인데 밀림 · 성능 격리 실패입니다. 이때 kv_cache_util_at_worst·queued_at_worst 가 같은 accel_pool 의 worst_minute pool_metrics 값인지, kv_cache_util_prev_hour_avg·queued_prev_hour_avg 가 그 분 앞 60분 평균인지 다시 계산합니다. 고객사 C 는 10:17 분 0.96·37건, 앞 60분 평균 0.47·4.9건입니다. 10. other_tenant_requests_prev_hour_avg 가 앞 60분 동안 센 다른 tenant_id 요청 수 ÷ 60 인지, other_tenant_requests_ratio 가 other_tenant_requests_at_worst ÷ other_tenant_requests_prev_hour_avg 인지 다시 계산합니다. 고객사 C 는 2,196 ÷ 60 = 36.6, 91 ÷ 36.6 = 2.5 이고, 고객사 M 은 1,158 ÷ 60 = 19.3, 58 ÷ 19.3 = 3.0 입니다. 11. worst_minute 의 pool_metrics 값이 null 이면 kv_cache_util_at_worst 와 queued_at_worst 도 null 입니다. 고객사 M 의 15:41 분이 이 경우이고, 판정은 요청 로그로 정해져 성능 격리 실패 그대로입니다. 12. verdict 가 성능 격리 실패가 아니면 kv_cache_util_at_worst 부터 other_tenant_requests_ratio 까지 일곱 필드는 null 이고, verdict 가 SLO 안에서 처리됨이면 worst_minute·worst_minute_misses 도 null 입니다. 13. evidence 의 table·field 가 request_logs·tenant_contracts·pool_metrics 에 실재하는 컬럼인지 대조합니다. 14. evidence 에서 agg 가 없는 value 가 그 row 의 그 컬럼 값과 같은지, agg 가 count 인 value 가 filter 조건으로 센 행 수인지 확인합니다. 15. summary 와 notes 가 존댓말로 끝나는지 확인합니다. [수동 검증 · 사람이 봅니다] 1. 성능 격리 실패로 걸린 시간대에 SLA 크레딧을 줄지는 기술 지원 담당자가 고객사 담당자와 계약서를 보고 정합니다. 2. 같은 분에 계약을 넘겨 보낸 다른 테넌트에게 상한을 걸지, 그 테넌트를 다른 풀로 옮길지는 플랫폼 운영 쪽이 정합니다. 3. 성능 격리 실패 시간대에 모델 재배포나 노드 장애가 겹쳤는지는 배포 이력을 보고 사람이 확인합니다. 4. 고객사 담당자에게 화면을 보여주기 전에 notes·summary 에 다른 테넌트의 tenant_id·tenant_name 이 나오지 않는지 봅니다. [평가 러너 · 2주차] 1. 부하 생성기에 원인을 심은 48개 시간대(성능 격리 실패·계약 동시성 초과·긴 입력·정상 각 12개)에서 verdict 가 심은 원인과 같은 시간대 수를 셉니다. 2. 게이트웨이에 테넌트별 동시성 상한을 걸고 같은 시나리오를 다시 돌려, 처음 성능 격리 실패였던 시간대 가운데 SLO 안에서 처리됨으로 바뀐 수를 셉니다. [판정식에 더한 정의 · 오너 확인 항목] 1. 근거로 지목할 요청을 가리키려면 요청마다 id 가 있어야 하고 화면에는 tenant_id 대신 사람이 읽는 테넌트 이름이 떠야 해서, 카드 inputs 에 없던 request_logs.request_id 와 tenant_contracts.tenant_name·model 을 더했습니다. 2. 시간대는 arrived_at 기준 정시부터 한 시간이고, 판정 단위는 tenant_id·endpoint_id·시간대입니다. 3. '1% 를 넘고' 는 slo_miss_count × 100 > request_count 로, '절반 이상' 은 해당 수 × 2 ≥ slo_miss_count 로 정수끼리 비교합니다. slo_miss_pct 는 화면 표시용 반올림 값이라 비교에 쓰지 않습니다. 4. 동시 요청 수는 같은 tenant_id·endpoint_id 요청끼리 셉니다. 계약이 엔드포인트마다 있기 때문입니다. 요청이 속한 분은 arrived_at 의 분이고, 분의 동시 요청 수는 그 분 안에서 잰 최댓값입니다. 5. 위반이 몰린 분은 SLO 를 넘긴 요청이 가장 많이 든 분이고, 같으면 이른 분입니다. 직전 1시간은 그 분 앞 60분이며 pool_metrics 평균은 값이 있는 분만으로 냅니다. 6. 다른 테넌트는 같은 accel_pool 의 다른 tenant_id 요청 수로만 보여주고 이름은 쓰지 않습니다. 이 화면은 고객사 담당자도 보기 때문입니다. 7. accel_pool 은 request_logs 에만 있어서, 한 엔드포인트가 한 시간 안에 두 풀을 오가면 그 시간대의 accel_pool 과 '다른 테넌트 요청 수' 를 셀 풀이 정해지지 않습니다. 엔드포인트는 한 accel_pool 에 고정된 것으로 보고, 한 시간대 안에서 다른 풀로 찍힌 줄은 읽지 못한 줄로 빼 표 위 알림에 줄 번호를 남깁니다.
근거: 멀티테넌트 환경에서 성능 격리, SLA/SLO, 사용량 계측 및 자원 최적화를 구현합니다.