# 온프레미스 모델 벤치마크 지시서

## 1. 목적

온프레미스 환경에서 오픈웨이트 LLM의 배포 가능성을 평가하고, GPU 구성별 성능과 비용 효율성을 측정한다.

검증 목표는 다음과 같다.

* vLLM 호환성
* 모델 로딩 가능 여부
* context 길이별 필요 GPU 메모리·KV cache 용량
* GPU 메모리 요구량
* KV cache 용량
* 최대 동시 sequence 수
* TTFT(Time To First Token)
* Prefill throughput
* Decode throughput
* Aggregate throughput
* 동시 사용자 증가에 따른 성능 변화
* GPU 구성별 가격 대비 처리량

최종 목표는 다음 매트릭스를 실측 데이터로 만드는 것이다.

`Model × GPU × GPU Count × Context Length × Concurrency → Memory / TTFT / Throughput / Cost`

이 데이터는 향후 온프레미스 시스템의 하드웨어 SKU를 결정하는 근거로 사용한다.

## 2. 평가 대상

목표 매트릭스가 `Model × GPU`이므로 평가 대상도 모델과 하드웨어 두 축으로 나눠 관리한다.

### 모델

평가 대상 모델은 다음 문서를 참조한다.

* 모델 선정 근거와 후보군: [on-premise-llm-recommendations.md](on-premise-llm-recommendations.md)

Tier 1/2 구분, 모델별 우선 테스트 GPU 구성, 측정 목적은 위 문서의 후보군을 기준으로
`configs/benchmark-matrix.yaml`에 반영한다. 새 모델을 추가할 때는
모델 YAML(`configs/models/`)을 추가하는 것으로 충분하다(8.1절).

OpenRouter의 모델 성능 평가는 이 프로젝트 범위 밖이며 혼합하지 않는다.

### 하드웨어

평가 대상 GPU 종류·수량은 `configs/hardware/`의 YAML로 정의하고, 모델과의 조합은
`configs/benchmark-matrix.yaml`에 반영한다. 실제 재고·가격은 이 문서나 YAML에 고정하지 않고
RunPod MCP `get-capacity`/`get-gpu-type` 조회로 실행 시점에 확인한다(5절).

과거 실험 기록의 모델별 GPU 구성과 목적은 [benchmark-report.md](benchmark-report.md)에 보존되어 있다.

## 3. 기술 스택

가능하면 모든 실험에서 동일한 software stack을 사용한다.

* RunPod
* Docker
* NVIDIA Container Toolkit
* vLLM 최신 stable 버전
* Python 3.11 이상
* Hugging Face Hub
* OpenAI-compatible vLLM API
* `gpu-monitor` subcommand (nvidia-smi 기반 GPU 사용률·메모리 CSV 기록 — 8절)
* Python benchmark scripts

모델별 특수 요구사항 때문에 stable vLLM에서 실행할 수 없는 경우에만 별도 버전을 사용한다.

그 경우 반드시 결과 metadata에 다음을 기록한다.

* vLLM version
* Docker image
* CUDA version
* NVIDIA driver
* 모델-specific option
* custom wheel 또는 nightly 사용 여부

## 4. 프로젝트 구조

다음 구조로 운영한다.

```text
onprem-benchmark/
├── README.md
├── .env.example
├── pyproject.toml
├── model-benchmark-instruction.md      # 본 문서 — 실행 기록 포함
│
├── benchmark/                          # 단일 스크립트 패키지 — 모든 기능의 진입점
│   ├── cli.py                          # 전체 구현. 섹션 docstring이 기능 문서 (8절)
│   ├── __main__.py                     # python -m benchmark <subcommand>
│   └── __init__.py
│
├── scripts/                            # 로컬 전용 보조 (docker·세션 기록)
│   ├── export_session_record.py        # 세션 기록 보존
│   ├── sync_cache_to_runpod.sh         # Phase 3: HF 외 체크포인트를 S3로 동기화
│   └── download_model_cache.sh         # Phase 3: 로컬 docker volume cache 준비
│
├── configs/
│   ├── benchmark-matrix.yaml           # 모델×GPU 매니페스트
│   ├── models/                         # 모델별 YAML (6절)
│   └── hardware/                       # GPU별 YAML
│
├── infra/
│   └── runpod/                         # 도메인 라이브러리: 가격·리전·payload 규칙
│
├── tests/
├── results/                            # runs/<실행>/remote/ raw, summary.csv, cost-ledger.json
└── records/                            # 세션 전사본 (추적하지 않음)
```

RunPod 제어는 MCP 도구로 수행한다(5절). 서버 기동은 선기동 템플릿 startCmd가,
실행 감시는 MCP `stream-pod-logs`, Pod 정리는 MCP `stop-pod`/`terminate-pod`가 담당한다.

`benchmark/`는 패키지다. 테스트가 import하고 (`from benchmark.cli import ...`),
측정 subcommand는 Pod 안에서 `python -m benchmark remote`로 실행되기 때문이다.
기능은 전부 `cli.py` 안의 섹션으로 나뉘고, 섹션 docstring과 함수 docstring이
기능 문서 역할을 한다. `scripts/`는 docker 기반 캐시 준비와 세션 기록처럼
로컬 전용 보조만 둔다.

## 5. RunPod

RunPod 제어부는 호스팅 Runpod MCP 서버(`https://mcp.getrunpod.io/`)를 통해 수행한다.
인증은 OAuth이며 API 키를 코드나 환경 변수에 저장하지 않는다.
대상 체크포인트는 공개 저장소만 사용하므로 Pod에는 `PUBLIC_KEY` 외의 자격 증명을 전송하지 않는다.

### 인증

다음의 정보로 RunPod에 접근할 수 있는지 확인한다.

* RUNPOD_API_KEY
* AWS_ACCESS_KEY_ID
* AWS_SECRET_ACCESS_KEY
* SSH Key: 현재 시스템에 등록된 SSH 키. Public key가 RunPod에 등록되어 있어야 함.

`HF_TOKEN`이 필요한 비공개/gated 모델은 현재 범위에서 제외한다.
`.env` 파일은 Git에 commit하지 않는다.

### MCP 도구 매핑

| 작업 | MCP 도구 | 비고 |
|---|---|---|
| Pod 생성 | `create-pod` | 선기동 템플릿 + GPU 종류/수/리전 지정 |
| Pod 조회·상태 | `get-pod`, `list-pods` | IP·포트, 실제 과금 요율, 상태 |
| Pod 중지·삭제 | `stop-pod`, `terminate-pod` | terminate는 결과 회수 확인 후에만 |
| 부팅 로그·사망 감지 | `stream-pod-logs` | 엔진 사망 시 즉시 중단. 죽은 프로세스를 타임아웃까지 폴링하지 않는다 |
| GPU 재고·가격 | `get-capacity`, `get-gpu-type` | 생성 전 실제 과금 요율 확인 |
| 템플릿 관리 | `list-templates`, `create-template` | 선기동 템플릿 생성·조회. 템플릿은 GPU를 점유하지 않는다 |
| Network volume | `create-network-volume` 등 | 100GB 이상 대형 모델에만 사용 |

### Pod 생성 기능 (선기동 방식)

모든 실행 단계에서 vLLM은 컨테이너 기동과 함께 선기동된다.
오케스트레이터가 서버를 띄우는 별도 단계는 없다.

* 이미지: 공식 `vllm/vllm-openai` — vLLM 사전 설치, 설치 비용 0초
* startCmd: `vllm serve …`를 백그라운드로 선기동한 뒤 sshd를 포그라운드로 실행
* ENTRYPOINT 교체 필수: `dockerEntrypoint: ["/bin/bash", "-lc"]`. 빈 리스트는 null로 저장되어 이미지 ENTRYPOINT(`vllm serve`)가 살아남고, startCmd가 서버의 인자로 흡수되어 sshd가 실행되지 않는다
* sshd 부트스트랩: `PUBLIC_KEY` 환경 변수의 공개키를 authorized_keys로 등록. 벤치마크 클라이언트 실행과 아티팩트 회수는 SSH로 수행한다
* 파라미터: `--gpu-type --gpu-count --container-image --volume-size --name`은 MCP `create-pod` 필드로 전달한다
* GPU 종류를 hard coding하지 않고 `get-capacity` 조회로 현재 사용 가능한 GPU를 선택한다

### 제어부 규칙 (기존 실행에서 검증됨)

| 규칙 | 내용 |
|---|---|
| 리전 enum ≠ 재고 목록 | 재고 조회에 나타나도 Pods API가 거부하는 리전이 있다. 허용 목록과의 교집합만 사용 |
| 볼륨 리전 고정 | network volume은 Pod를 볼륨 리전에 고정한다. 대상 GPU가 없는 리전의 볼륨은 스케줄 불가 |
| 가격 의미 | `lowestPrice(gpuCount: N)`는 GPU 1장 단가가 아니라 Pod 전체 요율이다. GPU 수를 다시 곱하지 않는다 |
| 서빙 이름 일치 | 서버가 등록한 모델명과 벤치마크 요청명이 어긋나면 GPU 과금 중에 전 요청 404 |
| 재고 소진 | `no instances currently available`만 재시도 대상. 그 외 오류는 즉시 중단 |
| 엔트리포인트 교체 | 템플릿 생성(MCP·GraphQL)에는 entrypoint 필드가 없다. 이미지 ENTRYPOINT(`vllm serve`)가 살아남아 startCmd가 서버 인자로 흡수되고 크래시 루프가 된다. 선기동 Pod는 REST v1 pod create(`dockerEntrypoint`/`dockerStartCmd`)로만 생성한다 |
| 지출 한도 | 계정 시간당 USD 80 한도는 동시 실행 Pod 요율 합계에 걸린다. 생성 전 `list-pods`로 현재 요율 합계를 확인 |
| 완료 동작 | 기본은 stop. terminate는 결과 회수와 아티팩트 무결성 확인 후 명시적으로만 수행 |

`infra/runpod/`는 benchmark 도구가 참조하는 도메인 라이브러리다(가격·재고 조회,
리전 검증, payload 규칙). Pod 제어·생성은 이 계층을 거치지 않고 MCP 도구로 수행한다.

### 비용 안전장치

GPU 비용이 크므로 이 부분은 필수다.

Pod의 lifecycle을 다음 상태로 명시적으로 관리한다.

```text
CREATED
BOOTSTRAPPING
MODEL_DOWNLOADING
SERVING
BENCHMARKING
COMPLETED
FAILED
STOPPED
TERMINATED
```

다음 안전장치를 구현한다.

1. benchmark가 끝나면 자동 stop 가능
2. 오류 발생 시에도 finally block에서 stop 가능
3. 명시적인 `--terminate-after-run` 옵션 제공
4. 기본값은 terminate하지 않고 stop
5. 최대 실행시간 timeout 설정
6. 실행 시작 시 예상 시간당 GPU 비용 기록
7. 실행 종료 시 실제 실행시간과 추정 비용 기록

Pod의 삭제/terminate는 destructive action이므로 코드상 명확히 분리한다.

RunPod에서 실제 유료 GPU를 생성하거나 terminate하는 단계에 도달하면 코드와 dry-run 결과를 먼저 검토한다.
구현과 로컬 테스트는 진행하되, 실제 고비용 multi-GPU 인스턴스를 무분별하게 생성하지 않는다.
특히 GLM-5.3-Flash H200 ×4/×8 및 DeepSeek-V3.2 H200 ×8 실험은 자동화 시스템이 Qwen3.8-27B 단일 GPU 환경에서 검증된 이후 실행한다.

## 6. 모델 설정

각 모델은 YAML로 정의한다.

예:

```yaml
name: glm-5.3-flash
hf_repo: zai-org/GLM-5.3-Flash

serve:
  dtype: auto
  tensor_parallel_size: auto
  max_model_len: model_maximum
  gpu_memory_utilization: 0.90
  enable_prefix_caching: true
  kv_cache_dtype: fp8

benchmark:
  context_ratios:
    - 0.125
    - 0.25
    - 0.5
    - 1.0

  concurrency:
    - 1
    - 2
    - 4
    - 8
    - 16
```

모델의 maximum context는 임의로 정하지 말고 Hugging Face `config.json` 또는 vLLM이 읽은 실제 값을 기록한다.

### vLLM

기본적으로 OpenAI-compatible server를 띄운다.

개념적으로 다음 형태다.

```bash
vllm serve MODEL \
    --tensor-parallel-size N \
    --max-model-len MAX_MODEL_LEN \
    --gpu-memory-utilization 0.90 \
    --enable-prefix-caching
```

실제 CLI option은 설치된 vLLM 버전에서 유효한지 확인하여 사용한다.

모델에 따라 FP8 KV cache가 안정적으로 지원되면 우선 사용한다.

그렇지 않으면 BF16/FP16 cache로 fallback하고 결과에 표시한다.

## 7. 데이터 수집

vLLM startup log를 저장한다.

특히 다음 정보를 parse하여 structured data로 저장한다.

* model architecture
* model dtype
* model weight memory
* max_model_len
* tensor parallel size
* pipeline parallel size
* available KV cache memory
* GPU KV cache size in tokens
* CPU KV cache size
* maximum concurrency at max_model_len
* graph capture memory
* GPU memory utilization

vLLM이 startup 시 출력하는 maximum concurrency 값은 hardware sizing의 핵심 데이터로 사용한다.

## 8. 자동화 스크립트

모든 진입점은 통합 CLI 하나다: `python -m benchmark <subcommand>`.
subcommand 뒤의 옵션은 담당 모듈의 것이며 `--help`로 확인한다.

Dry run 예:

```bash
python -m benchmark plan \
  --model glm-5.3-flash-h200x4 \
  --hardware h200x4 \
  --dry-run
```

전체 matrix는 매니페스트로 검증한다.

```bash
python -m benchmark preflight --matrix configs/benchmark-matrix.yaml
```

| subcommand | 용도 | 실행 위치 |
|---|---|---|
| `plan` | 매트릭스 설계·dry-run | 로컬 |
| `remote` | context×concurrency 측정 | Pod 내부 (SSH) |
| `code-agent` | 코드 에이전트 workload | Pod 내부 (SSH) |
| `preflight` | 리전·가격·볼륨 사전 검증 | 로컬 |
| `summarize` | summary·보고서 생성 | 로컬 |
| `timing` | 단계별 시간 보고 | 로컬 |
| `ledger` | 실험별 비용 원장 | 로컬 |
| `sysinfo` | 시스템·GPU 정보 수집 | Pod 내부 (SSH) |
| `gpu-monitor` | GPU 사용률·메모리 CSV 기록 | Pod 내부 (SSH) |
| `cache-fill` | Phase 3: network volume 채우기 | 로컬 |
| `cache-verify` | Phase 3: 볼륨 무결성 검증 | 로컬 |
| `cache-prepare` | Phase 3: 재고 확정 후 볼륨 생성 | 로컬 |
| `watch-download` | 전송 진행률·속도·ETA 감시 | 로컬 |

### 8.1 구현 원칙

1. 모델 이름, GPU, context length, concurrency를 코드에 hard coding하지 않는다.
2. 모든 실험은 configuration driven 방식으로 만든다.
3. RunPod API와 benchmark logic을 분리한다.
4. vLLM 실행과 benchmark를 분리한다.
5. 향후 새로운 오픈웨이트 모델을 YAML 하나 추가하는 것만으로 benchmark할 수 있게 만든다.

### 8.2 표준 실행 절차 (GPU 구성 1건 측정)

사전 조건: `python -m benchmark preflight`가 해당 구성을 CREATABLE로 판정하고,
실행 중 Pod 요율 합계 + 이번 요율이 시간당 USD 80 한도 안에 있다(5절).

1. **리전 선택** — 세 집합의 교집합에서 가용성이 가장 높은 리전을 고른다.
   * Pods API 허용 리전 (`preflight`의 허용 목록 — 재고 조회와 다른 집합)
   * S3 network volume 지원 리전 (`storageSupport`)
   * 대상 GPU 재고 리전 (MCP `get-gpu-type`의 리전별 availability)
   세 집합은 모두 시점에 따라 변하므로 실행마다 다시 확인한다.
   가용성이 전부 LOW면 감시 없이 생성 시도 자체를 탐침으로 쓴다(5절).
   볼륨은 Pod를 리전에 고정하므로 GPU를 바꿔가는 시리즈 실행에서는
   GPU별 교집합이 달라진다. 교집합이 비면 볼륨 없이 다운로드하거나(6절 임계값 미만),
   GPU별 볼륨을 새로 만든다.
2. **모델 캐시 준비** (100GB 이상은 필수, 미만은 선택 — 6절).

   ```bash
   # 볼륨 생성은 MCP create-network-volume (선택된 리전)
   python -m benchmark cache-fill --model M --volume-id V --data-center-id R --confirm-create
   python -m benchmark cache-verify --model M --volume-id V --confirm-create
   ```

   CPU Pod(USD 0.06/hour)가 데이터센터 대역폭으로 HF에서 받는다. GPU 과금 구간에
   다운로드가 포함되지 않는다. 29GB 실측: 약 3분, USD 0.003.
3. **GPU Pod 생성** — REST v1 경로로만 가능하다(위 엔트리포인트 교체 규칙).
   * startCmd: `vllm serve <local_model_path> …` 선기동 + sshd 부트스트랩 + `ssh-keygen -A`
   * `networkVolumeId` 연결, `dataCenterIds`는 볼륨 리전 단일값
   * 서빙 등록명 = `local_model_path` (볼륨 마운트 시) — 서빙 이름 일치 규칙
4. **부팅 검증** — SSH 접속 → `sysinfo` → `gpu-monitor`를 nohup으로 기동 →
   `/health` 폴링. 엔진 사망이 보이면 타임아웃까지 기다리지 않고 즉시 중단한다.
   이어서 smoke test로 실제 추론 가능 여부를 확인한다: `/v1/models` 정상 응답,
   짧은 prompt 생성 성공, tool/OpenAI-compatible response 형태 정상, GPU error·NaN·
   server crash 없음. smoke test가 실패하면 이후 측정 단계(9절)로 진행하지 않는다.
5. **측정** — `remote` / `code-agent` subcommand를 Pod 안에서 실행한다(9절).
6. **회수·정리** — 아티팩트를 scp로 회수하고 필수 산출물(`vllm-startup.log`,
   `system-info.txt`, `gpu-monitor.csv`) 무결성을 확인한 뒤 `stop`, 재실행 계획이
   없으면 `terminate`. 볼륨은 페이즈 종료 후 삭제한다(로컬 원본은 유지).
   `ledger`와 `summarize`로 비용·요약을 갱신한다.

### 8.3 Dry Run

실제 RunPod 인스턴스를 만들지 않고 다음 내용을 출력하는 dry-run 기능을 반드시 제공한다.

```bash
--dry-run
```

출력:

* 생성 예정 GPU
* GPU 개수
* 예상 시간당 가격
* 대상 모델
* 예상 disk requirement
* benchmark case 개수
* 자동 stop/terminate 여부

### 8.4 실패 복구

모델 다운로드가 완료되었지만 benchmark가 실패한 경우 같은 volume을 재사용할 수 있게 한다.

큰 모델을 다시 다운로드하지 않도록 RunPod network volume 또는 persistent volume 사용을 검토한다(5절).
단, persistent storage 비용도 결과에 기록한다.

## 9. Benchmark

실행 순서(어떤 모델·GPU부터 시작하는지)는 9.1에서 다룬다. 측정 자체는 Phase 순서로 진행한다:
smoke test(8.2절 부팅 검증) 통과 후에만 9.3 벤치마크 매트릭스를 실행한다. prefix caching 자체를
껐다 켜는 비교는 9.3 축 3(도구 사용)의 하위 항목으로 포함된다. 각 run마다의 측정 기록과 판정
기준은 9.2에서 다룬다.

### 9.1 실험 순서

비용을 줄이기 위해 모델마다 가장 작고 저렴한 GPU 구성부터 시작하고, 그 구성이 성공했을 때만 다음으로 큰 구성으로 확장한다.
모델 로딩이나 maximum context 자체가 불가능하면 즉시 다음 구성으로 넘어간다. GPU 재고 소진이나 vLLM 미지원처럼
근본적으로 막히면 무기한 대기하지 않고 우선순위가 더 낮은 모델·구성으로 넘어간다.

모델별 실제 GPU 확장 순서와 우선순위는 이 문서가 아니라 `configs/benchmark-matrix.yaml`(2절)에서 관리한다.
이미 완료되었거나 차단된 조합의 판정은 [benchmark-report.md](benchmark-report.md)를 참조한다.

아직 측정된 적 없는 모델을 새로 투입할 때는 다음 순서를 따른다.

1. `configs/benchmark-matrix.yaml`에서 아직 판정이 없는 조합 중 가장 저렴한 GPU 구성을 고른다.
2. 그 조합 하나를 9.3의 도구 미사용 조건 전체 케이스(context 비율 × concurrency)로 end-to-end 완성해 자동화 경로를 먼저 검증한다.
3. 검증된 경로로 같은 모델의 나머지 GPU 구성, 이어서 다음 모델로 확장한다.

### 9.2 측정

각 run마다 다음 두 종류의 정보를 함께 기록한다.

#### 성능 지표

```text
timestamp
model
model_revision
vllm_version
cuda_version

gpu_model
gpu_count
gpu_memory_total

weight_dtype
kv_cache_dtype

max_model_len
input_tokens
output_tokens
concurrency
tool_use
output_length_forced
prefix_caching_enabled
cache_hit_ratio

ttft_p50
ttft_p95

inter_token_latency_p50
inter_token_latency_p95

prefill_tokens_per_sec
decode_tokens_per_sec
aggregate_tokens_per_sec

peak_gpu_memory
kv_cache_memory
kv_cache_usage_percent

gpu_utilization_avg
gpu_utilization_peak

successful_requests
failed_requests
oom_count
preemption_count

run_duration_seconds
gpu_hourly_cost
estimated_run_cost
```

#### 측정값

각 셀(context 비율 × concurrency × 도구 사용 여부)마다 다음을 기록한다.

* 성공/실패
* 실제 input tokens
* TTFT
* prompt processing time
* prefill tokens/sec
* decode tokens/sec
* peak GPU memory
* KV cache usage
* 요청 종료 후 cache 상태
* OOM 여부
* preemption 여부

#### 반복과 분산

각 셀은 최소 3회 반복 측정한다. TTFT·decode 속도는 반복 간 변동이 판정을 뒤집을 수
있다 — 실측으로 확인됐다: B200 32K 동접 4·16에서 동일 조건 재측정 간 SLA 판정이
엇갈렸다(benchmark-report.md 2026-08-29).

* raw에는 반복별 값을 모두 남기고, summary에는 평균과 최소·최대를 함께 기록한다.
* SLA 판정(판정 기준)은 평균으로 한다.
* 반복 간 편차가 SLA 판정을 뒤집는 셀은 VARIANCE로 표시한다. 재측정으로 변동 원인을
  확인하기 전까지 해당 셀의 단일 실행 수치를 결론의 근거로 삼지 않는다.

#### 재현성 메타데이터

```text
Hugging Face model revision / commit hash
vLLM git/version
Docker image digest
CUDA version
GPU driver
benchmark git commit
benchmark configuration
```

나중에 모델 또는 vLLM 업데이트 전후 결과를 비교할 수 있어야 한다.

#### 산출물

raw 결과에서 최종 문서까지 하나의 파이프라인으로 만든다. 모든 raw 결과는 삭제하지 않으며, 실패한 실험도 결과로 기록한다.

| 단계 | 위치 | 내용 |
|---|---|---|
| raw | `results/raw/*.json`, `results/runs/<실행>/remote/` | run 1건의 원본 |
| 누적 | `results/runs.csv` | 전체 run 누적 |
| summary | `results/summary.csv` | 자동 생성 summary matrix: `Model, GPU, Count, Context, Concurrency, Tool Use, TTFT p50, tok/s/user, Aggregate tok/s, Peak VRAM, Cost/hr` + 모델별 하드웨어 권장안 |
| 비용 원장 | `results/cost-ledger.json` | 실험별 실제 실행시간과 추정 비용 (5절) |
| 보고서 | `results/reports/<model>-<gpu>.md` | 모델·GPU 조합별 Markdown 보고서 |
| hardware sizing | `docs/hardware-sizing.md` | 전체 benchmark 결과를 반영해 자동 생성·갱신 |
| 세션 로그 | [benchmark-report.md](benchmark-report.md) | 세션별 실행 로그와 판정 (실측값이 없는 항목은 `미측정`으로 기록) |
| 복구본 | `results/recovered/` | 유실된 raw의 복원본, 출처 명시 |

보고서 구조:

```markdown
# <Model> / <GPU> ×<Count>

## Configuration

## vLLM startup information

## Maximum context

## KV cache capacity

## Concurrency

## Latency

## Throughput

## GPU memory

## Failures / Preemptions

## Estimated operating cost

## Recommended production workload

## Conclusion
```

Conclusion에는 다음 중 하나를 명시한다.

```text
Recommended
Conditionally Recommended
Not Recommended
```

hardware-sizing.md는 다음 열을 갖는다. 값은 전부 실측 또는 추정으로 표시하고 둘을 명확히 구분한다.

| Deployment Tier | Model | Hardware | Max Context | Recommended Concurrent Agents |
|---|---|---|---:|---:|

#### 판정 기준

초기에는 다음을 SLA parameter로 설정하고 이후 실험 결과에 따라 조정한다. 값을 코드에 hard coding하지 않는다.

```yaml
service_sla:
  ttft_p50_seconds: 5
  minimum_decode_tokens_per_second_per_user: 15
  maximum_error_rate: 0.01
```

이 SLA를 기준으로 결과를 분류한다.

##### PASS

* OOM 없음
* 요청 실패 없음
* preemption이 허용 범위
* TTFT가 설정 기준 이하
* 사용자당 decode 속도가 설정 기준 이상

##### MEMORY PASS / PERFORMANCE FAIL

VRAM에는 들어가지만 실서비스 throughput이 부족한 경우.

##### FAIL

* OOM
* server crash
* severe preemption
* unacceptable latency

이 구분은 중요하다.

"모델이 GPU에 올라간다"와 "서비스 가능한 하드웨어다"를 동일하게 취급하지 않는다.

### 9.3 벤치마크 매트릭스

전체 benchmark는 `context 비율 × concurrency × 도구 사용 여부` 3개 축의 매트릭스로 측정한다.
축 1·2의 정의와 진행 규칙은 두 도구 사용 조건이 공유하고, 입력·출력 내용만 다르다.

#### 축 1: context

각 context 길이를 실제로 서빙하는 데 필요한 GPU 메모리·KV cache 용량을 실측하는 것이 목적이다.
다음 context 비율을 사분위 단위로 테스트한다.

```text
1/4 (25%)
2/4 (50%)
3/4 (75%)
4/4 (100%)
```

예를 들어 maximum context가 1M이면:

```text
256K
512K
768K
1M
```

수준으로 테스트한다. 정확한 값은 모델 maximum context에서 계산한다.

#### 축 2: concurrency

concurrency를 1부터 2배씩 늘려가며(1, 2, 4, 8, 16, 32, 64, …) 진행한다. 상한을 미리 고정하지
않는다 — 소형 단일 GPU 구성은 16~32 근처에서 이미 막힐 수 있지만, H200 ×4/×8·B200 같은 다중
GPU 엔터프라이즈 구성은 실제 조직 배포에서 그보다 훨씬 많은 동시 사용자·에이전트를 감당해야
하므로 통과하는 한 64, 128, 256 이상까지 계속 두 배씩 올린다.

* 가장 낮은 context 비율(1/4): 실패할 때까지 concurrency를 계속 확장한다.
* 나머지 context 비율: concurrency 1부터 시작해, 서버가 감당하는 범위까지만 올린다.

한 context에서 상위 concurrency가 OOM·요청 실패·심각한 preemption으로 막히면 그 context는
더 높은 concurrency로 진행하지 않고, 해당 셀까지의 결과만 기록한다.

#### 축 3: 도구 사용 여부

* **미사용** — 일반 random token 벤치마크 입력을 사용한다. 이 조건이 하드웨어 sizing의 기준
  매트릭스이며, 축 1·2의 전체 조합을 여기서 실행한다.
* **사용** — 코드 에이전트 전형 프롬프트를 사용한다. 입력은 다음 특징을 갖는다.
  * 긴 source code
  * 여러 파일의 내용
  * 반복되는 prefix
  * tool 호출·tool 결과 형식 블록
  * reasoning + patch 생성 형태

  이 조건은 미사용 조건에서 이미 찾은 concurrency 상한을 처음부터 다시 찾지 않는다.
  concurrency 1과, 미사용 조건에서 PASS한 최대 concurrency 두 지점만 비교 측정해 tool 결과
  삽입이 prefix cache 적중률·TTFT·decode 속도에 미치는 영향을 확인한다.

  출력은 다음 길이를 측정한다.

  ```text
  512 tokens
  2K tokens
  8K tokens
  ```

  모델이 자연 종료로 목표 길이에 도달하지 못하는 경우가 있으므로(예: 실제로 71 토큰에서 조기
  종료된 사례), `ignore_eos`와 `min_tokens`로 정확한 길이를 강제하고, 강제 여부를 결과에 함께
  기록한다.

  ##### 접두사 캐시 비교

  이 조건(도구 사용)에서 동일 repository/context prefix를 가진 여러 요청을 만들어
  `prefix caching OFF` vs `ON`을 추가로 비교한다.

  * TTFT
  * input processing time
  * aggregate throughput
  * GPU utilization
  * cache hit ratio
  * memory usage

  이 결과는 실제 agent concurrency sizing에 반영한다.
