# 벤치마크 실행 기록

이 문서는 [model-benchmark-instruction.md](model-benchmark-instruction.md)의 지시에 따라 수행한 실험을 세션별로 기록한다.
이 절은 실험이 끝날 때마다 즉시 갱신한다. 실측값이 없는 항목은 추정값으로 대체하지 않고 `미측정`으로 기록한다.

원시 결과와 집계 파일은 이 문서가 아니라 다음 위치를 참조한다.

* 실행별 raw 데이터: `results/raw/`, `results/runs/<실행>/remote/`
* 누적 매트릭스: `results/runs.csv`, `results/summary.csv`
* 자동 생성 하드웨어 sizing: `docs/hardware-sizing.md`
* 비용 원장: `results/cost-ledger.json`
* 복구된 raw 원본: `results/recovered/` (출처 명시)

## 실험 진행 기록

### 2026-08-28 — 사전 인증 및 SSH 확인

| 항목 | 결과 |
|---|---|
| RunPod API 인증 | PASS (`GET /v1/pods`, HTTP 200) |
| Hugging Face 인증 | PASS (`GET /api/whoami-v2`, HTTP 200) |
| SSH 공개키 | PASS (ED25519, RunPod 등록 키 사용) |
| 비밀값 저장 여부 | 저장하지 않음; 환경 변수로만 사용 |

### 2026-08-28 — Qwen3.8-27B / RTX PRO 6000 ×1 dry-run

#### 설정

| 항목 | 값 |
|---|---|
| 모델 | `Qwen/Qwen3.8-27B-FP8` |
| 공식 네이티브 최대 컨텍스트 | 262,144 tokens |
| GPU | NVIDIA RTX PRO 6000 Blackwell Server Edition 96 GB ×1 |
| Cloud | Secure Cloud, on-demand |
| vLLM | 0.17.0 |
| KV cache dtype | FP8 |
| Context cases | 32K, 64K, 128K, 256K |
| Concurrency cases | 1, 2, 4, 8, 16 |
| 전체 조합 | 20건 + smoke test 1건 |
| 최대 실행시간 | 3,600초 |
| 완료 시 동작 | terminate가 아닌 stop |

#### 결과

| 항목 | 결과 |
|---|---|
| 로컬 구성 테스트 | PASS (2/2) |
| RunPod GPU 유형 지원 | PASS |
| 조회 시점 시간당 가격 | USD 1.69/hour |
| 최대 추정 GPU 비용 | USD 1.69 (스토리지 비용 제외) |
| 모델 다운로드 예상량 | 약 29 GB |
| 최초 온라인 dry-run | FAIL — Python 기본 HTTP client가 Cloudflare `403/1010` 응답 수신 |
| HTTP client 수정 후 재실행 | PASS — User-Agent 및 GraphQL 인증 방식 수정 |
| 최초 유료 실행 요청 | 중단 — 공개 모델에 불필요한 HF 토큰 원격 전송 구성 발견 |
| 보안 수정 | HF 토큰과 RunPod API 키를 Pod에 전송하지 않도록 수정; 공개 다운로드 사용 |
| 최초 Pod 생성 | 자동 중단 — dry-run 최저가 USD 1.69와 실제 Secure Cloud 생성가 USD 2.09 불일치 |
| 최초 Pod 실행시간/추정비용 | 약 60초 / USD 0.0347 |
| 최초 Pod 완료 동작 | stop 완료; 모델 설치 및 벤치마크 시작 전 중단 |
| Secure Cloud Pod resume | FAIL — 기존 호스트에 가용 GPU 없음; 추가 비용 발생 없음 |
| Community Cloud 대안 조회 | USD 1.69/hour, 재고 `Low`; 새 Pod 할당 필요 |
| 결과 파일 | `onprem-benchmark/results/reports/qwen3.8-27b-rtx-pro-6000-dry-run.json` |

#### 판정

`DRY-RUN PASS`. 유료 Pod 생성 전 조건을 충족했다. 아직 모델 로딩, 컨텍스트, 동시성 및 성능 값은 실측하지 않았다.

### 2026-08-28 — Serverless 하이브리드 방식 전환

Pod 실험만으로 전체 매트릭스를 수행하지 않고 다음과 같이 범위를 분리한다.

1. Serverless 예비 실험: smoke test, 최대 컨텍스트 요청, 동시성, TTFT, decode 및 aggregate throughput
2. Pod 최종 검증: vLLM 시작 로그, 실제 KV cache capacity, peak VRAM, GPU 통신 및 고정 하드웨어 재현성

전환 요청 시 실행 중이던 Community Cloud Pod는 약 37초 후 stop했다. 추정 컴퓨트 비용은 USD 0.0176이며 벤치마크는 시작되지 않았다. Secure/Community Pod 두 개 모두 `EXITED` 상태임을 확인했다.

### 2026-08-28 — Qwen3.8-27B / RTX PRO 6000 Serverless dry-run

#### 설정

| 항목 | 값 |
|---|---|
| 모델 | `Qwen/Qwen3.8-27B-FP8` |
| Worker image | `runpod/worker-v1-vllm:v2.25.2` |
| Worker vLLM | 0.20.2 |
| GPU | RTX PRO 6000 Blackwell Server Edition 96 GB ×1 |
| Flex worker | minimum 0, maximum 1 |
| Scale to zero | 사용 |
| Idle timeout | 300초 |
| Context cases | 32K, 64K, 128K, 256K |
| Concurrency cases | 1, 2, 4, 8, 16 |
| 전체 조합 | 20건 + smoke test 1건 |
| 실패 처리 | 한 context에서 실패하면 해당 context의 더 높은 동시성은 실행하지 않음 |
| 완료 처리 | Endpoint와 private template 삭제 |

#### 비용 및 안전장치

| 항목 | 결과 |
|---|---|
| Serverless Flex 단가 | USD 3.49/hour, 초 단위 과금 |
| 최대 worker 실행시간 | 3,600초 |
| 최대 추정 컴퓨트 비용 | USD 3.49 (스토리지 비용 제외) |
| 로컬 비밀값의 worker 환경 전달 | 없음 |
| Worker 자동 확장 상한 | 1 |
| 로컬 테스트 | PASS (2/2 및 Python/shell 구문 검사) |
| 판정 | `SERVERLESS DRY-RUN PASS` |

Serverless의 첫 짧은 요청은 cold start를 포함한 smoke test로 따로 기록하고, 이후 warm worker에서 context/concurrency 성능을 측정한다. Serverless 결과는 Pod의 고정 하드웨어 실측값과 구분한다.

### 2026-08-28 — Serverless 예비 실행 1

#### 실행 결과

| 실험 | 결과 | TTFT p50 | Decode tok/s | 비고 |
|---|---|---:|---:|---|
| Smoke / 4 input + 16 output | PASS | 300.22초 | 미산정 | 최초 image/model cold start 포함, 총 300.79초 |
| 32K / concurrency 1 | 예비 성공 | 5.44초 | 40.51 | 실제 input 32,624, output 128 |
| 32K / concurrency 2 | 예비 성공 | 1.39초 | 72.06 | 실제 input 합계 65,248, output 합계 256 |
| 32K / concurrency 4 | FAIL | 미측정 | 0 | HTTP 종료 후 token/usage가 없는 빈 stream, 약 301초 |
| 32K / concurrency 8 | 예비 성공 | 38.80초 | 228.33 | 실제 input 합계 260,992, output 합계 1,024 |

실행 도중 방법론 오류를 발견해 중단했으며 Endpoint와 template은 정상 삭제했다. 전체 경과시간 671.32초, 경과시간 기준 최대 비용 추정치는 USD 0.6508이다.

#### 데이터 품질 판정

`INVALID FOR FINAL COMPARISON`. 빈 stream을 성공으로 처리하는 판정 오류가 있었고, 모든 요청이 같은 prefix를 사용하여 일반 동시성 결과에 prefix cache 효과가 혼입됐다. raw JSON은 삭제하지 않고 보존하되 최종 하드웨어 표에는 사용하지 않는다.

다음 실행부터는 요청마다 첫 cache block을 다르게 만들고, 첫 token과 usage가 모두 존재해야 성공으로 판정한다. Prefix cache 성능은 별도 실험으로 분리한다.

### 2026-08-28 — Serverless 예비 실행 2

요청별 고유 prefix와 엄격한 성공 판정을 적용했으나, 첫 OpenAI streaming smoke 요청이 300.44초 후 token/usage 없이 종료됐다.

| 항목 | 결과 |
|---|---|
| Smoke test | FAIL — empty stream |
| 실행된 context/concurrency case | 없음 |
| 전체 경과시간 | 305.32초 |
| 경과시간 기준 최대 비용 추정 | USD 0.2960 |
| Endpoint/template cleanup | PASS |

원인은 약 5분인 최초 model cold start와 OpenAI streaming 연결의 약 300초 경계가 겹친 것으로 판단한다. 다음 실행에서는 RunPod native 비동기 `/run` 요청과 `/status` polling으로 worker를 먼저 준비한 뒤 warm worker에서 OpenAI-compatible smoke 및 성능 실험을 시작한다. Idle timeout은 600초로 늘리되 worker 상한은 1개로 유지한다.

### 2026-08-28 — Serverless 예비 실행 3

RunPod native 비동기 warmup은 성공했으나, 이어진 OpenAI-compatible smoke는 다시 실패했다.

| 항목 | 결과 |
|---|---|
| Native warmup | PASS — `COMPLETED`, 381.60초 |
| Warm OpenAI smoke | FAIL — 300.30초 후 empty stream |
| 실행된 context/concurrency case | 없음 |
| 전체 경과시간 | 686.94초 |
| 경과시간 기준 최대 비용 추정 | USD 0.6659 |
| Endpoint/template cleanup | PASS |

OpenAI 호환 경로는 첫 실행에서 한 차례 성공했지만 후속 두 실행에서 같은 300초 empty stream이 재현되어 `UNSTABLE`로 판정한다. Serverless 예비 매트릭스는 공식 native 비동기 `/run` 및 `/status`로 측정한다. 이 방식에서는 queue delay와 execution time은 실측할 수 있지만 TTFT는 측정하지 않으며, TTFT는 Pod 최종 검증에서 측정한다.

### 2026-08-28 — Serverless 예비 실행 4

Native API만 사용하는 매트릭스를 시도했다. warmup job은 성공했지만 이어진 32K 단일 요청이 worker에 할당되지 않고 queue에 고정됐다.

| 항목 | 결과 |
|---|---|
| Native warmup | PASS — `COMPLETED`, 315.77초 |
| 32K / concurrency 1 | 중단 — warmup 완료 후에도 `inQueue=1`, `inProgress=0` 지속 |
| 전체 경과시간 | 548.23초 |
| 경과시간 기준 최대 비용 추정 | USD 0.5315 |
| Endpoint/template cleanup | PASS |

#### Serverless 종합 판정

`NOT RECOMMENDED FOR THIS BENCHMARK`. 모델 로딩과 단발 생성은 성공했으므로 기본 vLLM 호환성은 확인했다. 그러나 OpenAI 경로는 반복적으로 300초 empty stream을 반환했고, native 경로도 warmup 이후 후속 job이 queue에 고정되어 재현 가능한 매트릭스를 만들지 못했다. Serverless 결과를 온프레미스 하드웨어 sizing 근거로 사용하지 않는다.

Pod 최종 검증은 비용을 제한하기 위해 32K에서 concurrency 1/2/4/8/16을 측정하고, 64K/128K/256K는 concurrency 1로 최대 context 가능 여부를 확인하는 8-case 집중 실험으로 수행한다.

### 2026-08-28 — Pod 집중 검증 재고 확인

| 항목 | 결과 |
|---|---|
| 기존 Community Pod resume | FAIL — 기존 호스트 가용 GPU 없음, 추가 비용 없음 |
| Community 신규 RTX PRO 6000 | 가격/재고 없음 |
| Secure 신규 RTX PRO 6000 | USD 2.09/hour, stock `Low` |
| Secure H200 대안 | USD 4.59/hour, stock `Medium` |

모델과 목표 SKU를 유지하기 위해 H200으로 변경하지 않고 Secure RTX PRO 6000 신규 할당을 우선 시도한다. 전체 배포·설치·모델 로딩·벤치마크를 합친 실행시간은 3,600초로 강제 제한한다.

### 2026-08-28 — Pod 집중 검증 실행 1

Secure Cloud RTX PRO 6000 Pod에서 vLLM 0.17.0 설치와 공개 체크포인트 다운로드 및 서버 기동까지 검증했다. 다만 원격 명령의 SSH quoting 오류로 벤치마크 모듈을 프로젝트 디렉터리 밖에서 실행하여 측정 case 진입 전에 종료됐다.

| 항목 | 결과 |
|---|---|
| Pod | `wm73vtkvw452of` |
| GPU / 단가 | RTX PRO 6000 Blackwell 96 GB ×1 / USD 2.09/hour |
| vLLM 설치 | PASS — 0.17.0, `/workspace/venvs/vllm` |
| 모델 다운로드 | PASS — `Qwen/Qwen3.8-27B-FP8`, Hugging Face cache 약 31 GB |
| 모델 weight load | PASS — 66 shards, GPU weight memory 28.51 GiB |
| 최초 서버 기동 | FAIL — venv에 설치된 `ninja`가 PATH에 없어 FlashInfer JIT 실패 |
| PATH 수정 후 서버 기동 | PASS — health check 통과 후 benchmark 단계 진입 |
| 집중 측정 8 cases | 미실행 — `benchmark.remote_suite` 모듈 경로를 찾지 못함 |
| 결과 회수 | FAIL — scp의 원격 `/.` 경로 처리 오류 |
| 실행시간 / 추정비용 | 1,876.09초 / USD 1.0892 |
| 완료 처리 | PASS — Pod stop, Pod volume의 venv와 모델 cache 보존 |

#### 판정 및 후속 조치

`INFRASTRUCTURE VALIDATION PASS / BENCHMARK INVALID`. 모델과 262K 설정으로 서버가 준비되는 것까지 확인했지만 성능 측정값은 없다. 다음 실행 전에 다음을 수정한다.

1. venv `bin`을 PATH에 포함하고 서버 프로세스를 `nohup`으로 분리한다.
2. SSH `bash -lc`에 전달하는 전체 명령을 단일 shell argument로 quote한다.
3. 결과 회수에서 scp가 거부하는 trailing `/.`를 제거한다.
4. 공식 `vllm/vllm-openai:v0.17.0` 이미지의 private Pod template을 사용하고, 모델 cache는 Docker volume에서 준비한 뒤 RunPod network volume으로 동기화한다.

### 2026-08-28 — 재사용 Pod template 및 로컬 cache 준비

| 항목 | 결과 |
|---|---|
| RunPod template | 생성 완료 — `g30i33khnm` (`qwen3.8-27b-vllm-0.17`) |
| Container image | `vllm/vllm-openai:v0.17.0` 공식 이미지 |
| 모델 서버 설정 | 262,144 max context, FP8 KV cache, prefix cache, TP=1 |
| 모델 cache 경로 | `HF_HOME=/workspace/.cache/huggingface` |
| 로컬 cache 준비 | `codebasic/base` 컨테이너 + 기존 `hf-cache` Docker volume + `uvx hf download` |
| 로컬 GPU 할당 | 없음 |
| Hugging Face 인증 | Docker volume 내부 token 사용 확인 (`hf auth whoami` 성공); token 값/환경 전달 없음 |
| 로컬 다운로드 결과 | PASS — revision `017b9c7af6b5689d5dd426a76e0bc077eb5ca20a`, 29 GB |
| snapshot 검증 | PASS — 81 files, safetensors 66 shards, broken link 0 |
| 기존 RunPod network volume | 없음 |

Template 생성 자체에는 컴퓨트 비용이 없다. 새 Pod 간 모델 cache를 공유하려면 S3 API가 지원되는 데이터센터에 network volume을 별도로 만들고, 로컬 Docker volume의 완성된 snapshot만 `/models/Qwen3.8-27B-FP8`로 동기화한다. Network volume은 GPU 없이 S3 API로 채울 수 있지만 저장 용량 요금은 발생한다.

### 2026-08-28 — 기존 cache Pod 재개 시도

| 항목 | 결과 |
|---|---|
| 대상 Pod | `wm73vtkvw452of` |
| 재개 결과 | FAIL — 기존 host에 빈 GPU 없음 |
| 실행/측정 | 시작되지 않음 |
| 추가 컴퓨트 비용 | 없음 |

기존 Pod volume에 vLLM과 모델 cache가 남아 있어도 해당 host의 GPU가 다른 사용자에게 할당되면 재개할 수 없다. 따라서 재현 가능한 반복 실행에는 특정 Pod의 local volume이 아니라 template과 network volume 조합이 필요하다.

### 2026-08-28 — RunPod network volume 생성

| 항목 | 결과 |
|---|---|
| 볼륨 | `qwen3.8-27b-model-cache` |
| ID | `ndaq9lh2lg` |
| 데이터센터 | `US-WA-1` (S3 API 지원) |
| 크기 | 50GB |
| 예상 저장 비용 | 약 USD 3.50/month |
| 상태 | 생성 완료, 아직 모델 동기화 전 |

S3 API key가 설정되면 `onprem-benchmark/scripts/sync_cache_to_runpod.sh`가 로컬 `hf-cache` Docker volume의 완성된 snapshot만 네트워크 볼륨의 `/models/Qwen3.8-27B-FP8`로 동기화한다.

### 2026-08-28 — vLLM 버전 정책 갱신

초기 Pod 검증은 Qwen3.8 지원 최소선과 재현성 확보를 위해 vLLM `0.17.0`을 명시적으로 고정했다. 이는 로컬에 해당 버전이 있어서 선택한 것이 아니다. 다만 해당 실행은 SSH 경로 오류로 성능 측정이 무효였고, 최신 버전을 제한할 유효한 호환성 근거도 확인되지 않았다.

공식 최신 릴리스가 `v0.28.0`으로 갱신되어 현재 설정을 다음과 같이 변경했다.

| 항목 | 변경값 |
|---|---|
| vLLM | `0.28.0` |
| Pod image | `vllm/vllm-openai:v0.28.0-cu129` |
| 선택 이유 | RTX PRO 6000의 CUDA 12.9 환경과 정렬; 최신 Qwen3.8 수정/개선 포함 |
| RunPod template | `g30i33khnm`을 `qwen3.8-27b-vllm-0.28`로 업데이트 |
| 비교 원칙 | 0.17.0 이력은 실패한 인프라 검증으로 보존; 유효한 성능값은 0.28.0으로 재측정 |

참고: [vLLM v0.28.0 릴리스 노트](https://github.com/vllm-project/vllm/releases/tag/v0.28.0)는 CUDA 12.9 이미지 태그와 Qwen3.8 관련 변경을 명시한다.

### 2026-08-28 — 실험 프로파일별 Pod template 사전 생성

캐시 동기화와 병행해 실제 실험 프로파일별 private Pod template을 미리 생성했다. Template은 GPU를 예약하지 않으며, GPU 종류와 수는 Pod 생성 시 하드웨어 설정으로 선택한다.

| 실험 프로파일 | Template ID | 이름 | 용도 |
|---|---|---|---|
| Qwen3.8-27B 집중 검증 | `g30i33khnm` | `qwen3.8-27b-vllm-0.28` | 8 cases: 32K 동시성 1/2/4/8/16 + 64K/128K/256K 단일 |
| Qwen3.8-27B 전체 매트릭스 | `0ufi1szy89` | `qwen3.8-27b-full-vllm-0.28` | 20 context×concurrency 조합 |

두 template 모두 `vllm/vllm-openai:v0.28.0-cu129`, FP8 KV cache, 262,144 max context, `/workspace/models/Qwen3.8-27B-FP8` 경로를 사용한다.

### 2026-08-28 — Phase 3/4 전체 프로파일 및 template 사전 준비

Phase 4의 하드웨어 비교까지 누락되지 않도록 모델·GPU 조합을 먼저 매니페스트로 고정했다. 매니페스트는 `onprem-benchmark/configs/benchmark-matrix.yaml`에 있으며, 각 항목은 모델 설정(`configs/models/`)과 하드웨어 설정(`configs/hardware/`)을 조합한다. Template은 GPU를 점유하지 않으므로 실험 전에 모두 만들어 둘 수 있다.

| 계획 단계 | 모델/프로파일 | GPU 프로파일 | 상태 |
|---|---|---|---|
| Phase 2/4 기준선 | Qwen3.8-27B FP8 | RTX PRO 6000 ×1 | template `g30i33khnm`, Qwen cache 동기화 후 실행 |
| Phase 2 검증 | Qwen3.8-27B FP8 | H200 ×1, B200 ×1 | 설정/매트릭스 준비, 실행 대기 |
| Phase 3 | GLM-5.3-Flash FP8 | H200 ×4, B200 ×4, H200 ×8 | template 생성 완료, 모델 cache 미준비 |
| Phase 3 | DeepSeek-V3.2 FP8 | H200 ×8 | template 생성 완료, 모델 cache 미준비 |

생성된 Phase 3 template은 다음과 같다.

| 모델/구성 | Template ID | 이름 |
|---|---|---|
| GLM-5.3-Flash, H200 ×4 | `xrz76gfmrm` | `glm-5.3-flash-h200x4-vllm-0.28` |
| GLM-5.3-Flash, H200 ×8 | `l77374z8nz` | `glm-5.3-flash-h200x8-vllm-0.28` |
| DeepSeek-V3.2, H200 ×8 | `kykn14ya6f` | `deepseek-v3.2-h200x8-vllm-0.28` |

모델별 설정에는 공식 config의 최대 context를 반영했다. GLM-5.3-Flash는 최대 1,048,576 토큰, DeepSeek-V3.2는 최대 163,840 토큰이며, 실제 측정은 메모리 여유를 고려해 단계적으로 context를 늘린다. 모델 파일은 각각 별도 network volume에 동기화한 후 template의 `/workspace/models/...` 경로에 연결한다. GLM/DeepSeek는 수백 GB 규모이므로 현재 50GB Qwen volume을 재사용하지 않는다.

Phase 4 실행 순서는 다음으로 고정한다.

1. Qwen network volume 동기화 완료 확인 후 RTX PRO 6000 기준선과 H200/B200 단일 GPU 검증을 수행한다.
2. 각 실행은 지시서의 context × concurrency 매트릭스와 동일한 입력·출력·warmup 규칙으로 raw 결과를 먼저 이 문서에 기록한다.
3. GLM/DeepSeek cache와 대형 GPU Pod가 준비되면 Phase 3를 실행하고, 같은 측정 규칙으로 Phase 4 비교표를 갱신한다.
4. 모든 유효 결과가 모인 뒤에만 `book/open_weight_models.md`의 처리량·TTFT·하드웨어 sizing 표를 갱신한다. Serverless 예비 결과와 인프라 검증 실패 결과는 최종 비교 수치에서 제외한다.

이 시점에서 Phase 4는 **실험 설계·설정·template 사전 생성까지 완료**되었으며, 실제 성능 수집과 최종 보고서 반영은 cache 동기화 및 GPU 할당 후 수행한다.

### 2026-08-28 — Phase별 예산 계획

예산은 각 Pod를 최대 1시간(Phase 3의 대형 모델은 최대 2시간)만 실행하고, 측정 완료 즉시 stop한다는 상한으로 계획한다. 아래의 `P_B200`은 실험 시작 직전에 RunPod API에서 조회한 B200 시간당 가격이며, 가격·재고가 변할 수 있으므로 고정값으로 기록하지 않는다. Template 생성과 로컬 cache 다운로드는 GPU 비용이 없다.

| Phase | 실행 범위 | 컴퓨트 예산(상한 계산) | 저장 예산 |
|---|---|---:|---:|
| Phase 1 | RTX PRO 6000 smoke/인프라 검증 1회 | `1 × 2.09 = USD 2.09` | 기존 Qwen network volume `USD 3.50/month` |
| Phase 2 | Qwen RTX PRO 6000 + H200 + B200, 각 1시간 | `2.09 + 4.59 + P_B200` | Qwen volume만 유지 |
| Phase 3 | GLM H200×4, B200×4, H200×8 및 DeepSeek H200×8, 각 2시간 | `36.72 + 8×P_B200 + 73.44 + 73.44 = USD 183.60 + 8×P_B200` | GLM/DeepSeek 모델별 임시 volume. 측정 후 삭제 |
| Phase 4 | 유효한 조합 재실행 및 전체 matrix/report 생성 | Phase 2·3에서 확보한 raw 결과를 재사용하고, 실패 case만 동일 상한으로 재실행 | 최종 보고서만 보존, 모델 volume 삭제 |

Phase 4의 별도 실행 예산은 Phase 2/3에서 유효한 결과가 확보되면 0달러가 될 수 있다. 반대로 전체 7개 matrix 항목을 1시간씩 다시 실행하는 최악의 경우에는 `USD 98.48 + 5×P_B200`으로 계산한다. 모든 실행은 시작 전에 조회한 실제 단가와 예상 종료 시각을 run JSON에 기록하고, 예산 상한에 도달하면 자동 stop한다. 저장 비용은 컴퓨트 비용과 분리해 모델별 volume 생성·삭제 시점으로 산정한다.

#### 캐시 수명주기 정책

로컬 Docker volume을 원본(source of truth)으로 유지하고, 원격 RunPod network volume은 해당 페이즈의 Pod 실행 기간에만 사용하는 임시 복제본으로 운영한다. 페이즈의 모든 측정과 raw 결과 회수가 성공한 뒤에는 다음 순서로 정리한다.

1. 결과 JSON/CSV와 모델 revision을 로컬에 회수한다.
2. 대상 Pod를 stop하고, 더 이상 재실행할 계획이 없으면 원격 모델 경로와 network volume을 명시적으로 삭제한다.
3. 로컬 모델별 Docker volume은 유지해 나중에 필요한 모델만 다시 동기화한다.

원격 삭제는 자동화하지 않고 확인 절차를 거친 명시적 명령으로만 수행한다. 로컬 디스크가 부족하면 모델별 로컬 volume을 동시에 보관할 수 없으므로, GLM 동기화·측정·원격 삭제 후 DeepSeek를 준비하는 순차 운영으로 전환한다. 로컬 원본까지 삭제하면 이후 재동기화가 불가능하므로 삭제 대상은 항상 원격 복제본으로 제한한다.

#### GLM 우선 실행 예산 조정

사용자 승인 범위를 GLM-5.3-Flash로 제한하고 DeepSeek 실행은 보류한다. 앞서 제시한 Phase 3의 USD 183.60은 실제 할당액이 아니라 대형 모델 4개 프로파일을 각각 2시간 실행하는 최악의 상한이었다. GLM은 다음과 같이 짧은 단계로 측정한다.

| 단계 | 구성 | 목표 실행시간 | 비용 상한 |
|---|---|---:|---:|
| 1차 smoke + 핵심 matrix | H200×4 | 30분 | `USD 9.18` |
| 2차 비교 | B200×4 | 30분 | `2 × P_B200` |
| 3차 확장 | H200×8 | 30분 | `USD 18.36` |

따라서 GLM 3개 프로파일의 1차 예산 상한은 `USD 27.54 + 2×P_B200`이다. 30분 안에 모델 로딩이나 핵심 case가 끝나지 않는 경우에만 해당 프로파일을 1시간까지 연장하며, 이는 자동 실행 시간이 아니라 승인 가능한 최대치다. 캐시가 준비된 뒤에는 모델 다운로드 시간이 GPU 과금 구간에 포함되지 않도록 먼저 cache 상태를 검증하고 Pod를 시작한다.

### 2026-08-28 — Qwen cache 동기화 확인 및 Phase 1/2 할당 시도

Qwen network volume의 S3 목록과 로컬 cache를 대조했다.

| 항목 | 결과 |
|---|---|
| 원격 객체 수 | 81 |
| 원격 총 크기 | 30,890,049,597 bytes |
| 로컬 revision | `017b9c7af6b5689d5dd426a76e0bc077eb5ca20a` |
| 로컬 snapshot 크기 | 약 30GB |
| 동기화 판정 | PASS |

동기화된 volume을 연결해 Qwen Phase 1/2 집중 검증을 시작하려 했으나, RTX PRO 6000과 H200 모두 현재 요구 사양을 만족하는 가용 Pod가 없어 RunPod API가 `could not find any pods with required specifications`를 반환했다. 두 시도 모두 Pod가 생성되지 않아 컴퓨트 비용은 발생하지 않았다. 재고가 회복되면 동일한 network volume과 30분 상한으로 재시도한다.

### 2026-08-28 — 사전 준비물 검증 및 배치 결함 발견

사용자가 준비한 모델 캐시와 template을 먼저 검증했다.
둘 다 실재하지만, 그동안 Pod 할당이 계속 실패한 원인은 재고 부족이 아니라 **리전 배치 오류**였다.

#### 준비물 검증 결과

| 항목 | 결과 |
|---|---|
| Network volume S3 동기화 | PASS — 81 objects, 30,890,049,597 bytes, 로컬 snapshot과 일치 |
| Private template | PASS — 5개 모두 존재 (`g30i33khnm`, `0ufi1szy89`, `xrz76gfmrm`, `l77374z8nz`, `kykn14ya6f`) |
| Template image | 전부 `vllm/vllm-openai:v0.28.0-cu129` |

#### 근본 원인 — network volume 리전에 대상 GPU가 없음

데이터센터별 GPU 재고를 조회한 결과는 다음과 같다.

| 데이터센터 | storage | RTX PRO 6000 | H200 | B200 |
|---|---|---|---|---|
| EU-RO-1 | True | USD 1.69 / Low | 없음 | 없음 |
| EU-FR-1 | True | 없음 | USD 3.59 / Low | 없음 |
| EUR-IS-4 | True | 없음 | USD 3.59 / Low | 없음 |
| US-CA-2 | True | 없음 | USD 3.59 / Low | USD 5.98 / Low |
| US-CO-1 | True | 없음 | USD 3.59 / Low | 없음 |
| US-NC-2 | True | 없음 | 없음 | USD 5.98 / Low |
| **US-WA-1** | True | **없음** | **없음** | **없음** |

모델 캐시를 동기화한 `US-WA-1`에는 대상 GPU 세 종류가 모두 없다.
Network volume을 붙이면 Pod가 해당 리전에 고정되므로, 이 조합은 원리적으로 스케줄 불가능하다.
앞선 기록의 `could not find any pods with required specifications`는 전부 이 원인으로 설명된다.
RTX PRO 6000은 현재 `EU-RO-1`에서만 조회된다.

#### Template을 사용하지 않는 이유

생성된 5개 template은 유효하지만 현재 오케스트레이션과 실행 모델이 다르다.
오케스트레이터는 SSH 기반으로 프로젝트를 Pod에 복사하고 `vllm bench serve`를 Pod 안에서 실행한 뒤 결과를 회수한다.
공식 `vllm/vllm-openai` 이미지는 추론 서버 전용이라 sshd와 PUBLIC_KEY 연동이 없고, `dockerStartCmd`가 서버를 선기동해 준비 단계를 가로챈다.
따라서 이번 실행은 검증된 경로(`runpod/pytorch` 이미지 + SSH + vLLM 설치)를 유지한다.
Template은 삭제하지 않고 향후 이미지 기반 실행 모델용으로 보존한다.

#### 코드 수정

| 결함 | 수정 |
|---|---|
| payload에 `dataCenterIds` 없음 | 하드웨어 설정의 `data_center_ids`를 payload에 반영 |
| Network volume이 무조건 연결됨 | `use_network_volume` opt-in으로 변경, 리전 불일치 시 명시적 오류 |
| 서버 등록명과 벤치마크 요청명 불일치 | 서버는 `local_model_path`로 기동하고 벤치마크는 `hf_repo`를 요청해 전 요청 404이 되는 결함. `served_model_name()`으로 양쪽을 일치 |
| 가격 조회 범위 불일치 | cloud type·GPU 수·리전을 payload와 동일 조건으로 조회 (USD 1.69가 아니라 실제 과금가 USD 2.09) |
| 결과 회수 실패가 무시됨 | scp 종료코드와 필수 산출물 존재·크기를 검사해 `missing_artifacts`로 기록 |
| `max_model_len` 하드코딩 | `--max-model-len` 인자로 분리 (지시서 27.1) |

로컬 테스트는 7건 전부 통과했다.

#### 수정 후 dry-run

| 항목 | 값 |
|---|---|
| 모델 / 서빙 등록명 | `Qwen/Qwen3.8-27B-FP8` (양쪽 일치) |
| GPU / 리전 | RTX PRO 6000 ×1 / `EU-RO-1`, Secure, stock `Low` |
| 시간당 가격 | USD 2.09 (payload와 동일 조건 조회) |
| Network volume | 미연결 — Pod에서 공개 체크포인트 직접 다운로드 |
| Case 수 | 8건 + smoke test 1건 |
| 최대 실행시간 / 비용 상한 | 3,600초 / USD 2.09 |
| 완료 시 동작 | stop |
| 판정 | `DRY-RUN PASS` — 유료 생성 승인 대기 |

#### Network volume 처리 방침

`US-WA-1` 볼륨은 이번 실행에 사용할 수 없다.
선택지는 두 가지이며, 유료 실행 전에 결정한다.

1. 현행 유지 — 볼륨 미사용, Pod에서 29GB 직접 다운로드. 데이터센터 대역폭이라 GPU 과금 구간에서 수 분 수준이다.
2. `EU-RO-1`에 볼륨 재생성 후 재동기화. 가정용 업링크로 30GB 재업로드가 필요하고 저장 비용이 추가된다.

성공한 실행이 아직 한 건도 없으므로 1번을 우선한다.
반복 실행이 확정된 뒤에 2번으로 전환하는 편이 비용·시간 모두 유리하다.

### 2026-08-28 — Phase 1/2 유료 실행 시도 1 (EU-RO-1)

수정된 payload로 승인된 유료 실행을 시도했으나 RunPod가 인스턴스 할당을 거부했다.

| 항목 | 결과 |
|---|---|
| 요청 구성 | RTX PRO 6000 ×1, `EU-RO-1`, Secure, network volume 미연결 |
| dry-run 시점 재고 | `Low` |
| 생성 결과 | FAIL — `HTTP 500: create pod: There are no instances currently available` |
| 생성된 Pod | 없음 |
| 발생 비용 | USD 0.00 |
| 측정값 | 없음 |
| 확인된 Pod 목록 | 기존 3개 모두 `EXITED` 유지 |

이번 실패는 이전과 성격이 다르다.
이전의 `could not find any pods with required specifications`는 볼륨 리전 오류로 인한 **구성 결함**이었고, 이번 `no instances currently available`은 payload가 유효한 상태에서의 **일시적 재고 소진**이다.
즉 배치 수정은 유효했고, 남은 제약은 EU-RO-1의 RTX PRO 6000 실물 가용성뿐이다.

재고가 `Low`인 단일 리전 SKU이므로 자동 재시도 루프는 돌리지 않는다.
불필요한 생성 요청 반복과 예산 통제 상실을 피하기 위해, 재시도는 명시적 승인 또는 감시 후 1회 생성으로 제한한다.

### 2026-08-28 — 재고 감시 및 EU-RO-1 캐시 준비

#### 재고 감시

승인에 따라 재고 감시 후 1회 자동 생성 방식으로 전환했다.
`stockStatus`는 신뢰할 수 있는 신호가 아니다. 직전 실패 시점에도 값은 `Low`였으나 실제 할당은 거부됐다.
따라서 감시는 상태 조회가 아니라 생성 시도 자체로 수행한다.
생성이 거부되면 Pod가 만들어지지 않아 과금도 없으므로, 이 재시도의 비용은 API 호출뿐이다.

| 항목 | 값 |
|---|---|
| 구현 | `orchestrate.py --wait-for-capacity-seconds` |
| 재시도 대상 | `no instances currently available` 오류 한정 |
| 그 외 오류 | 즉시 중단 (맹목적 재시도 금지) |
| 폴링 간격 / 상한 | 120초 / 10,800초 |
| 과금 시계 시작 | Pod 생성 성공 시점 (감시 대기 시간은 비용에 산입하지 않음) |
| 성공 시 동작 | 승인된 8-case 실행 1회, 완료 후 stop |

#### EU-RO-1 캐시 준비

대기 시간을 활용해 대상 리전에 모델 캐시를 준비한다.
`US-WA-1` 볼륨은 해당 리전에 대상 GPU가 없어 재사용할 수 없다.

`sync_cache_to_runpod.sh`의 Qwen 하드코딩을 제거하고 `HF_REPO` 기반으로 일반화했다 (지시서 27.1, 27.10).
로컬 원본 캐시는 revision `017b9c7af6b5689d5dd426a76e0bc077eb5ca20a`로 온전하다.

| 항목 | 값 |
|---|---|
| 신규 볼륨 | `qwen3.8-27b-cache-euro1`, 50GB, `EU-RO-1` |
| 예상 저장 비용 | 약 USD 3.50/month |
| 상태 | 생성 대기 — 과금 리소스라 승인 필요 |

첫 실행은 볼륨 없이 진행되므로 Pod에서 직접 다운로드한다.
동기화가 끝나면 이후 반복 실행에서 `use_network_volume: true`로 전환해 다운로드 시간을 GPU 과금 구간에서 제거한다.

`US-WA-1` 볼륨(`ndaq9lh2lg`)은 사용할 수 없으므로 저장 비용만 발생한다.
캐시 수명주기 정책상 원격 삭제는 명시적 확인을 거쳐 수행한다.

#### venv 위치 수정

Network volume을 연결하면 `/workspace`가 곧 볼륨이므로, 기존 `/workspace/venvs/vllm` 설정은 vLLM venv(torch 포함 약 10~15GB)가 50GB 볼륨을 모델 가중치 31GB와 나눠 쓰게 만든다.
설치 도중 용량 부족으로 실패하면 이미 GPU 과금이 시작된 뒤다.
venv를 컨테이너 디스크의 `/opt/venvs/vllm`으로 옮겨 볼륨 용량과 분리했다.
이 수정으로 50GB 볼륨은 모델 전용으로 충분하다.

### 2026-08-28 — EU-RO-1 볼륨 생성 확인 및 동기화 재개

볼륨 생성 자체는 성공했으나 캐시 동기화가 수행되지 않아 볼륨이 비어 있었다.

| 항목 | 결과 |
|---|---|
| 신규 볼륨 | `qwen3.8-27b-cache-euro1` / `n9ja6mqq4f` / `EU-RO-1` / 50GB |
| 생성 판정 | PASS |
| 최초 S3 객체 수 | 0 — 동기화 미수행 |
| 조치 | `sync_cache_to_runpod.sh`를 `n9ja6mqq4f` 대상으로 재실행 |

기존 `US-WA-1` 볼륨(`ndaq9lh2lg`)은 대상 GPU가 없는 리전이라 사용할 수 없고 저장 비용만 발생한다.
삭제는 캐시 수명주기 정책에 따라 명시적 확인 후 수행한다.

### 2026-08-28 — 시간당 지출 한도 USD 80 검토

계정에 시간당 USD 80 한도가 적용된다.
이 한도는 총 지출이 아니라 **동시에 실행 중인 Pod들의 시간당 요율 합계**에 걸린다.

먼저 RunPod 가격 API의 의미를 확인했다.
`lowestPrice(gpuCount: N)`은 GPU 1장 단가가 아니라 **Pod 전체의 시간당 요율**이다.

| gpuCount | H200 조회값 |
|---:|---:|
| 1 | USD 4.59 |
| 2 | USD 9.18 |
| 4 | USD 18.36 |
| 8 | USD 36.72 |

이 값에 GPU 수를 다시 곱하면 4배·8배로 과대계상된다.
`orchestrate.py`와 `run_suite.py`는 조회 시 `gpuCount`를 넘기고 결과를 그대로 Pod 요율로 사용하므로 계산은 올바르다.

#### 계획 대비 한도 검토

| 실험 구성 | Pod 요율 | 2시간 비용 | 한도 대비 |
|---|---:|---:|---|
| Qwen3.8-27B / RTX PRO 6000 ×1 | USD 2.09 | USD 4.18 | OK |
| Qwen3.8-27B / H200 ×1 | USD 4.59 | USD 9.18 | OK |
| Qwen3.8-27B / B200 ×1 | USD 6.79 | USD 13.58 | OK |
| GLM-5.3-Flash / H200 ×4 | USD 18.36 | USD 36.72 | OK |
| GLM-5.3-Flash / B200 ×4 | 재고 없음 (추정 USD 27.16) | — | OK 예상 |
| GLM-5.3-Flash / H200 ×8 | USD 36.72 | USD 73.44 | OK |
| DeepSeek-V3.2 / H200 ×8 | USD 36.72 | USD 73.44 | OK |

가장 큰 단일 구성인 H200 ×8이 USD 36.72/hour로, 한도까지 USD 43.28/hour 여유가 있다.
**모든 실험은 순차 실행을 전제로 한도 안에 들어온다.**

다만 한도는 동시 실행 합계에 걸리므로 다음 조합은 초과한다.

* H200 ×8 두 개를 동시 실행하면 USD 73.44로 아직 한도 내지만, 여기에 B200 ×1만 더해도 USD 80.23으로 초과한다.
* 종료되지 않은 Pod가 남아 있으면 새 실행이 한도를 넘길 수 있다.

#### 지출 요율 안전장치

한도를 문서상 원칙이 아니라 코드 차원의 차단으로 구현했다.

| 항목 | 내용 |
|---|---|
| 구현 | `orchestrate.py`의 `check_spend_rate()` |
| 검사 시점 | Pod 생성 전 |
| 검사 대상 | 현재 `RUNNING` Pod 요율 합계 + 이번 실행 요율 |
| 기본 한도 | USD 80/hour (`--max-hourly-rate-usd`) |
| 초과 시 | Pod를 만들지 않고 즉시 중단 |
| 기록 | run state에 `projected_account_hourly_rate` 저장 |

이 검사는 다른 세션에 남은 Pod까지 계정 전체를 조회하므로, 방치된 Pod로 인한 초과도 사전에 차단한다.
로컬 테스트 9건 전부 통과했다.

### 2026-08-28 — 리전 후보 재조사 및 감시 범위 확대

앞서 `EU-RO-1`을 RTX PRO 6000의 유일한 리전으로 기록한 것은 틀렸다.
초기 조사는 `gpuCount: 1` 단일 조건과 특정 시점의 응답만 본 것이라 후보를 과소평가했다.
데이터센터 전체를 GPU 수 조건별로 재조사한 결과는 다음과 같다.

| DC | S3 | RTX PRO 6000 ×1 | H200 ×1 | H200 ×4 | H200 ×8 | B200 ×1 |
|---|---|---|---|---|---|---|
| EU-CZ-1 | no | 1.69 | - | - | - | - |
| EU-RO-1 | yes | 1.69 | - | - | - | 5.98 |
| EUR-IS-1 | yes | 1.69 | - | - | - | - |
| EUR-IS-2 | no | 1.69 | - | - | - | - |
| EUR-IS-4 | yes | - | 3.59 | - | - | - |
| EUR-IS-5 | no | - | 3.59 | 14.36 | 28.72 | - |
| US-CA-2 | yes | - | 3.59 | - | - | 5.98 |
| US-CO-1 | yes | - | 3.59 | - | - | - |
| US-GA-2 | no | - | 3.59 | - | - | - |
| US-KS-2 | yes | 1.69 | - | - | - | - |
| US-NC-1 | no | - | 3.59 | 14.36 | - | - |
| US-NC-2 | yes | 1.69 | - | - | - | 5.98 |
| US-NE-1 | yes | 1.69 | - | - | - | - |
| US-PA-1 | no | 1.69 | - | - | - | - |
| US-TX-6 | no | - | - | - | - | 5.98 |

RTX PRO 6000은 8개 리전에서 제공되며, 8곳 모두 Secure Cloud 기준 동일한 USD 2.09/hour다.
한 리전으로 고정한 것은 할당 확률만 낮추는 선택이었다.

다만 8개를 그대로 쓰면 Pod 생성이 `HTTP 400`으로 거부된다.
GPU 재고 조회에 나타나는 데이터센터와 Pods API가 허용하는 데이터센터 목록이 다르기 때문이다.
`US-NC-2`, `US-NE-1`, `US-PA-1`은 재고는 있으나 Pod 생성 enum에 없다.
따라서 실제 사용 가능한 리전은 다음 5곳이다.

| 리전 | S3 | 비고 |
|---|---|---|
| EU-RO-1 | yes | 모델 캐시 볼륨 위치 |
| EUR-IS-1 | yes | |
| US-KS-2 | yes | |
| EU-CZ-1 | no | |
| EUR-IS-2 | no | |

이 400 오류는 재고 오류가 아니므로 재시도 없이 즉시 중단됐고 Pod는 생성되지 않았다.
테스트에 Pods API 허용 목록 검사를 추가해 같은 실수를 사전에 막는다.

#### US-WA-1 감시 여부

감시하지 않는다.
`US-WA-1`은 A100-SXM4-80GB 한 종류만 취급하며 대상 GPU 세 종류가 애초에 배치되어 있지 않다.
즉 재고 소진이 아니라 미취급이므로 기다려도 나오지 않는다.
기존 `US-WA-1` 볼륨을 재활용할 방법은 없다.

#### Phase 3 리전 제약

H200 ×8은 현재 `EUR-IS-5` 한 곳에서만 조회되며, 이 리전은 S3 network volume을 지원하지 않는다.
따라서 DeepSeek-V3.2와 GLM H200 ×8 구성은 볼륨 기반 캐시를 쓸 수 없고 Pod에서 직접 모델을 받아야 한다.
B200 ×4는 현재 어느 리전에서도 조회되지 않는다.

### 2026-08-28 — 미사용 Pod 정리

정지된 Pod도 볼륨과 컨테이너 디스크가 남아 저장 비용이 계속 발생하므로 정리했다.
컴퓨트 과금은 `EXITED` 상태에서 발생하지 않지만 디스크는 유지된다.

| Pod | 상태 | 볼륨 | 컨테이너 디스크 | 조치 |
|---|---|---:|---:|---|
| `wm73vtkvw452of` | EXITED | 100GB | 100GB | terminate |
| `0xsrmd2xyx36vd` | EXITED | 100GB | 100GB | terminate |
| `2p897dbuoy87cw` | EXITED | 100GB | 100GB | terminate |
| `dnut79c3htab6h` | RUNNING | 100GB | 100GB | 유지 — 측정 진행 중 |

총 300GB 볼륨과 300GB 컨테이너 디스크를 회수했다.
`wm73vtkvw452of`에는 이전 실행의 vLLM venv와 모델 캐시가 남아 있었으나, 해당 호스트 GPU를 재확보할 수 없어 재개가 반복 실패했으므로 실질적으로 접근 불가능한 데이터였다.
동일 모델은 이미 로컬 Docker volume과 EU-RO-1 network volume에 있다.

#### terminate 안전장치 추가

`terminate_pod.py`가 `--confirm-terminate`만 요구하고 Pod 상태는 확인하지 않았다.
측정이 진행 중인 Pod를 실수로 파괴할 수 있으므로 `RUNNING` Pod는 거부하도록 했다.

| 항목 | 내용 |
|---|---|
| 기본 동작 | `RUNNING` Pod terminate 거부 |
| 우회 | `--allow-running` 명시 필요 |
| 검증 | 실행 중인 `dnut79c3htab6h`에 대해 거부 확인 |

`US-WA-1` network volume(`ndaq9lh2lg`, 50GB)은 여전히 사용 불가 상태로 저장 비용만 발생한다.
Pod가 아닌 별도 리소스이므로 이번 정리에는 포함하지 않았고, 삭제는 확인 후 수행한다.

### 2026-08-28 — 실행 가능 범위 판정 및 지식의 스크립트화

#### 실제 생성 가능한 구성

재고 목록이 아니라 Pods API가 허용하는 리전만 놓고 판정했다.

| 구성 | 판정 | 요율 | 가능 리전 |
|---|---|---:|---|
| Qwen3.8-27B / RTX PRO 6000 ×1 | CREATABLE | USD 2.09 | EU-RO-1, EUR-IS-1, EU-CZ-1, EUR-IS-2, EU-NL-1, US-NC-1 |
| Qwen3.8-27B / H200 ×1 | CREATABLE | USD 4.59 | US-GA-2, US-NC-1 |
| Qwen3.8-27B / B200 ×1 | CREATABLE | USD 6.79 | US-CA-2 |
| GLM-5.3-Flash / H200 ×4 | BLOCKED | USD 18.36 | US-NC-1 |
| GLM-5.3-Flash / B200 ×4 | BLOCKED | — | 없음 |
| GLM-5.3-Flash / H200 ×8 | BLOCKED | — | 없음 |
| DeepSeek-V3.2 / H200 ×8 | BLOCKED | — | 없음 |

Phase 2는 전부 실행 가능하다.
Phase 3은 두 가지 이유로 막혀 있다.

1. H200 ×8과 B200 ×4는 Pods API가 허용하는 리전 어디에도 현재 재고가 없다.
2. GLM H200 ×4는 유일한 가능 리전이 `US-NC-1`인데, 이 리전은 S3를 지원하지 않아 캐시 볼륨을 미리 채울 수 없고, 모델 약 700GB에 비해 Pod 디스크가 300GB뿐이다.

따라서 Phase 3은 재고 회복과 대용량 스토리지 확보가 선행되어야 한다.
디스크 부족은 재고와 무관한 설정 문제이므로 하드웨어 설정의 디스크 크기를 먼저 조정해야 한다.

#### preflight 스크립트

매번 같은 함정을 다시 발견하지 않도록 판정 로직을 코드로 고정했다.

```bash
python3 -m benchmark.preflight
```

검사 항목은 Pods API 리전 enum, 리전별 재고와 실제 과금 요율, network volume 리전 일치, 모델 용량 대비 Pod 디스크, S3 지원 여부, 서빙 이름 일치다.
결과는 `results/preflight.json`에 저장한다.

#### Pod 정리 스크립트

```bash
python3 -m infra.runpod.cleanup_pods                     # 미리보기
python3 -m infra.runpod.cleanup_pods --confirm-terminate # 유휴 Pod 삭제
```

`RUNNING` Pod는 절대 대상에 넣지 않으며 회수 가능한 디스크 용량을 먼저 보고한다.

#### RunPod 공식 플러그인 및 프로젝트 스킬

공식 안내(`https://docs.runpod.io/agent-setup.md`)에 따라 Claude Code 플러그인을 설치했다.

| 항목 | 결과 |
|---|---|
| marketplace | `runpod/runpod-plugins-official` 추가 완료 |
| plugin | `runpod@runpod` 1.2.0, `enabled` |
| 남은 작업 | 사용자가 `/reload-plugins` 실행 후 `/mcp` → runpod → 로그인 (대화형이라 자동화 불가) |

프로젝트 고유의 시행착오는 별도 스킬로 정리했다.

| 항목 | 값 |
|---|---|
| 위치 | `.claude/skills/runpod-vllm-benchmark/SKILL.md` |
| 이름 | 공식 `runpod` 스킬과 충돌하지 않도록 분리 |
| 내용 | preflight 우선 실행, 리전 enum 불일치, 볼륨 리전 고정, 가격 의미, 서빙 이름 일치, venv 위치, 디스크 용량, 재고 재시도 기준, USD 80 상한, serverless 한계 |

스킬에는 새로 알아낸 사실을 계속 반영한다.

### 2026-08-28 — 대화 기록·토큰·비용 원장

#### 대용량 모델 캐시 정책

대용량 체크포인트는 반드시 네트워크 볼륨에 먼저 동기화한 뒤 연결한다.
GPU 과금 구간에서 수백 GB를 내려받는 것은 파일 전송에 GPU 요금을 내는 것과 같다.

| 항목 | 내용 |
|---|---|
| 임계값 | 약 100GB 이상 (`NETWORK_VOLUME_REQUIRED_ABOVE_GB`) |
| 구현 | `check_cache_policy()` — Pod 생성 전 차단 |
| 개별 지정 | 모델 설정의 `require_network_volume`로 재정의 가능 |
| 예외 처리 | S3 지원 리전에 해당 GPU 재고가 없으면 그 구성은 BLOCKED로 두고 Pod 다운로드로 우회하지 않는다 |

Qwen3.8-27B(약 29GB)는 임계값 미만이라 Pod 직접 다운로드를 허용한다.
GLM-5.3-Flash(약 700GB)와 DeepSeek-V3.2(약 690GB)는 정책상 볼륨 없이는 실행되지 않는다.

#### 세션 기록 및 토큰

```bash
python3 scripts/export_session_record.py
```

원본 JSONL, 읽을 수 있는 Markdown 전사본, 토큰 집계를 `records/`에 저장한다.
자격증명 문자열이 검출되면 기록을 거부한다.

| 항목 | 값 |
|---|---:|
| 세션 | `b1d188dc-902a-4751-bc9a-32cf91b9cc09` |
| 전사 항목 수 | 671 |
| 렌더링된 대화 | 375 |
| 출력 토큰 | 328,453 |
| 비캐시 입력 토큰 | 572 |
| 캐시 생성 토큰 | 613,911 |
| 캐시 조회 토큰 | 42,625,318 |
| 캐시 포함 총계 | 43,568,254 |
| 자격증명 검사 | clean |

#### 실험별 비용 원장

```bash
python3 -m benchmark.cost_ledger
```

측정에 실패한 시도도 비용의 일부이므로 모두 남긴다.

| 실험 | 종류 | 시간(초) | 요율 | 비용 | 결과 |
|---|---|---:|---:|---:|---|
| RTX PRO 6000 secure 최초 Pod | pod | 60 | 2.09 | 0.0347 | aborted |
| RTX PRO 6000 community Pod | pod | 37 | 1.69 | 0.0176 | aborted |
| Serverless 시도 1 | serverless | 671 | 3.49 | 0.6508 | failed |
| Serverless 시도 2 | serverless | 305 | 3.49 | 0.2960 | failed |
| Serverless 시도 3 | serverless | 687 | 3.49 | 0.6659 | failed |
| Serverless 시도 4 | serverless | 548 | 3.49 | 0.5315 | failed |
| Pod 집중 검증 실행 1 | pod | 1,876 | 2.09 | 1.0892 | infrastructure-only |
| create 거부 (재고) | pod | 0 | 2.09 | 0.0000 | failed |
| create 거부 (리전 enum) | pod | 0 | 2.09 | 0.0000 | failed |

컴퓨트 누적 `USD 3.2857`이며, 이 중 실측값을 만든 비용은 `USD 0.0000`이다.
스토리지는 볼륨 2개로 `USD 7.00/month`가 나가고 있으며, `US-WA-1` 볼륨은 사용 불가 상태다.

이 수치는 진행 중인 `dnut79c3htab6h` 실행을 포함하지 않는다. 종료 시 원장에 추가한다.

### 2026-08-28 — EU-RO-1 캐시 동기화 완료 및 무결성 확인

| 항목 | EU-RO-1 (`n9ja6mqq4f`) | US-WA-1 참조 (`ndaq9lh2lg`) | 로컬 원본 |
|---|---:|---:|---:|
| 객체 수 | 81 | 81 | 81 |
| 바이트 | 30,890,049,597 | 30,890,049,597 | 동일 |
| revision | `017b9c7a…` | `017b9c7a…` | `017b9c7a…` |

판정 `SYNC VERIFIED`.

#### 파일 구성 확인

로그의 "66 shards"가 일반적인 `model-0000N-of-000NN` 형식과 달라 실제 파일 목록을 확인했다.
이 체크포인트는 레이어 단위 분할 방식을 쓴다.

| 파일 | 수 | 내용 |
|---|---:|---|
| `layers-0..63.safetensors` | 64 | 트랜스포머 레이어별 가중치 |
| `mtp.safetensors` | 1 | multi-token prediction 모듈 |
| `outside.safetensors` | 1 | 레이어 외부 가중치(임베딩·norm·head) |
| 합계 | 66 | 기록된 shard 수와 일치 |

`outside.safetensors`는 비정상 파일이 아니라 이 저장소의 명명 규칙이다.
로컬 `ls`가 80개로 보이는 것은 `.gitattributes`가 숨김 파일이기 때문이며, 동기화는 이를 포함해 81개를 복사한다.
`preprocessor_config.json`과 `video_preprocessor_config.json`이 있어 멀티모달 구성임을 확인했다.

이 볼륨은 이후 RTX PRO 6000 반복 실행에서 `use_network_volume: true`로 연결한다.
연결 시 `network_volume_id`를 `n9ja6mqq4f`, `network_volume_datacenter_id`를 `EU-RO-1`로 함께 변경해야 리전 검사를 통과한다.

### 2026-08-28 — Pod 실행 2 (EU-CZ-1): FlashInfer 커널 부재로 실패

리전 확대 직후 `EU-CZ-1`에 Pod가 할당됐다. 설치와 모델 다운로드는 정상이었으나 vLLM 엔진이 기동하지 못했다.

| 항목 | 결과 |
|---|---|
| Pod / 리전 | `dnut79c3htab6h` / `EU-CZ-1` |
| GPU / 단가 | RTX PRO 6000 Blackwell ×1 / USD 2.09 |
| vLLM 설치 | PASS — 0.28.0, `/opt/venvs/vllm` |
| 모델 다운로드 | PASS — 29GB, `Qwen/Qwen3.8-27B-FP8` |
| 가중치 로드 | PASS |
| 엔진 기동 | FAIL — `RuntimeError: FlashInfer backend is not available` |
| 측정 case | 0건 |
| 결과 회수 | PASS — `vllm-startup.log`, `system-info.txt` 회수 (`gpu-monitor.csv`는 SERVING 미도달로 미생성) |
| 실행시간 / 비용 | 1,962.12초 / USD 1.1391 |

#### 원인

로그의 결정적 두 줄은 다음과 같다.

```text
Using FLASHINFER attention backend out of potential backends: ['FLASHINFER', 'TRITON_ATTN'].
FlashInfer resolved query dtypes: ... decode_backend=xqa, kv_cache_dtype=torch.float8_e4m3fn, arch=sm120
```

vLLM은 FlashInfer를 자동 선택했고 sm120(Blackwell) + FP8 KV cache 조합에서 XQA decode 커널을 요구했다.
`flashinfer-python` 패키지는 설치되지만 컴파일된 커널을 포함하지 않는다.
커널은 별도 wheel(`flashinfer-cubin`, `flashinfer-jit-cache`)로 배포되며, 없으면 설치 시점이 아니라 **엔진 기동 시점에** 실패한다.
즉 모델 다운로드까지 GPU 과금을 다 치른 뒤에야 드러난다.

`TRITON_ATTN`이 대안으로 존재한다고 로그에 명시되어 있으므로 이 하드웨어에서 서빙 자체가 불가능한 것은 아니다.

#### 더 큰 손실 요인

엔진은 09:34:49에 죽었지만 `wait_for_server.sh`는 1,800초 타임아웃까지 죽은 프로세스를 계속 폴링했다.
실제 실패 판정에 3분이면 충분한 것을 33분 과금했다. 비용 USD 1.1391 중 약 USD 0.95가 이 대기였다.

#### 수정

| 수정 | 내용 |
|---|---|
| 사망 감지 | `wait_for_server.sh`가 `vllm.pid` 생존을 확인해 프로세스가 죽으면 즉시 종료(exit 2). 로컬 검증에서 60초 대신 1초 |
| 커널 사전 설치 | `install_vllm.sh`가 torch의 CUDA 버전을 감지해 `flashinfer-cubin`과 `flashinfer-jit-cache`를 설치. 실패해도 비치명적 |
| 진단 자료 | `flashinfer show-config` 출력을 `flashinfer-config.txt`로 회수 |
| 백엔드 폴백 | 기동 실패 시 같은 Pod 안에서 `TRITON_ATTN` → `FLASH_ATTN` 순으로 재시도. Pod를 새로 만들지 않으므로 추가 할당 대기가 없다 |
| 로그 보존 | 재시도 시 startup 로그를 덮어쓰지 않고 append |

`HEALTH_URL`을 환경 변수로 분리해 대기 로직을 로컬에서 검증할 수 있게 했다.

#### 누적 비용

컴퓨트 누적 `USD 4.4248`이며, 이 중 실측값을 만든 비용은 여전히 `USD 0.0000`이다.

### 2026-08-28 — Pod 실행 3: 사망 감지는 성공, FlashInfer 버전 불일치로 실패

| 항목 | 결과 |
|---|---|
| Pod | `aas73ibxamud7a` |
| 기동 시도 | 3회 모두 `engine died` (exit 2) |
| 측정 case | 0건 |
| 실행시간 / 비용 | 363.02초 / USD 0.2108 |

#### 사망 감지 효과

같은 성격의 실패가 직전 실행에서는 1,962초/USD 1.1391이었고 이번에는 363초/USD 0.2108이었다.
죽은 프로세스를 타임아웃까지 폴링하지 않게 되면서 실패 비용이 약 5분의 1로 줄었다.
수정이 의도대로 작동했다.

#### 새 실패 원인 — 내가 넣은 수정이 유발

```text
RuntimeError: flashinfer-cubin version (0.6.17) does not match
flashinfer version (0.6.16.post3).
```

`flashinfer-cubin`을 버전 지정 없이 설치해 최신 0.6.17이 들어갔고, 설치된 flashinfer 0.6.16.post3와 어긋났다.
이 검사는 엔진 초기화 시점에 걸리며 **어떤 attention backend를 쓰든 무조건 중단시킨다.**
따라서 `TRITON_ATTN` 폴백 2회도 백엔드 문제가 아니라 같은 버전 검사에 막혔고, Triton은 공정하게 시험되지 못했다.

교훈: 이 companion 패키지는 버전이 어긋나느니 아예 없는 편이 낫다.

#### 배포 버전 확인

| 패키지 | 사용 가능 버전 |
|---|---|
| `flashinfer` (설치됨) | 0.6.16.post3 |
| `flashinfer-cubin` | 0.6.16.post4, 0.6.17 — **0.6.16.post3 없음** |
| `flashinfer-jit-cache` (cu130) | 0.6.16.post3+cu130 — **정확히 일치** |

#### 수정

| 수정 | 내용 |
|---|---|
| 정확한 고정 | 설치된 flashinfer 버전으로 `==` 고정해 설치 |
| 불일치 시 제거 | 일치 버전이 없으면 설치하지 않고 기존 것도 제거 |
| 최종 폴백 | 3번째 시도에서 flashinfer companion 패키지를 제거한 뒤 `TRITON_ATTN`으로 기동 |
| 진단 강화 | `pip list`의 flashinfer 항목을 `flashinfer-config.txt`에 함께 기록 |

`flashinfer-jit-cache`는 정확히 일치하는 버전이 있으므로 XQA 커널이 확보될 가능성이 높고, 실패하더라도 3번째 시도에서 Triton이 깨끗한 환경에서 시험된다.

#### 누적 비용

컴퓨트 누적 `USD 4.6356`, 실측값을 만든 비용은 `USD 0.0000`이다.

### 2026-08-28 — Pod 실행 4: 첫 실측 성공

지시서 29절의 최초 목표 조합을 end-to-end로 완료했다.

| 항목 | 결과 |
|---|---|
| Pod | `8xa8r1nfn6zw3y` |
| 기동 | 1회차 성공 — FLASHINFER, FP8 KV cache (폴백 불필요) |
| 측정 case | 8/8 성공, 실패 요청 0 |
| 누락 아티팩트 | 없음 |
| 실행시간 / 비용 | 764.04초 / USD 0.4436 |
| 판정 | `MEASURED` |

flashinfer 버전을 정확히 고정하자 첫 시도에서 바로 기동했다. 폴백 체인은 사용되지 않았다.

#### vLLM startup (지시서 9절)

| 항목 | 값 |
|---|---|
| 모델 가중치 | 28.51 GiB |
| Available KV cache | 51.31 GiB |
| GPU KV cache size | 1,615,048 tokens |
| Maximum concurrency at 262,144 | 6.16x |
| graph capture | 0.76 GiB |
| engine 초기화 | 102.11초 |

#### 측정값

| Context | 동접 | 성공 | TTFT p50 (s) | TTFT p99 (s) | ITL p50 (ms) | Decode tok/s | Aggregate tok/s |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 32,768 | 1 | 1/1 | 4.12 | 4.12 | 22.4 | 18.42 | 4,714 |
| 32,768 | 2 | 2/2 | 2.52 | 4.31 | 24.7 | 34.38 | 8,797 |
| 32,768 | 4 | 4/4 | 3.28 | 8.51 | 26.7 | 42.89 | 10,974 |
| 32,768 | 8 | 8/8 | 4.20 | 16.85 | 29.2 | 49.54 | 12,676 |
| 32,768 | 16 | 16/16 | 6.76 | 33.86 | 36.5 | 53.10 | 13,587 |
| 65,536 | 1 | 1/1 | 6.38 | 6.38 | 23.0 | 13.80 | 7,063 |
| 131,072 | 1 | 1/1 | 18.31 | 18.31 | 24.5 | 5.99 | 6,136 |
| 262,144 | 1 | 1/1 | 59.32 | 59.32 | 27.4 | 2.04 | 4,182 |

peak VRAM 87,779 / 97,887 MiB, 평균 GPU 사용률 68.4%, OOM 0건.

#### p95 필드 결함

지시서 15절은 `ttft_p95`를 요구하지만 vLLM은 p95를 내보내지 않고 p99를 낸다.
`p95_ttft_ms`를 읽던 코드는 오류 없이 빈 열을 만들고 있었다.
p99를 읽어 p99로 기록하도록 수정했다. 저장된 raw JSON에 원본이 남아 있어 재실행 없이 값을 복원했다.

#### 판정

`Conditionally Recommended`.
262K 컨텍스트까지 서빙되고 실패·OOM이 없으나, SLA(TTFT p50 5초, 사용자당 15 tok/s)를 만족하는 구간은 32K에서 동접 2까지다.
결과는 `docs/hardware-sizing.md`에 통합돼 있다.

#### 누적 비용

| 항목 | 값 |
|---|---:|
| 컴퓨트 누적 | USD 5.0792 |
| 그중 실측값 생산 | USD 0.4436 |
| 스토리지 | USD 7.00/month |

### 2026-08-28 — 미사용 볼륨 정리 및 Phase 2 확장

#### 볼륨 삭제

| 볼륨 | 리전 | 조치 |
|---|---|---|
| `ndaq9lh2lg` (`qwen3.8-27b-model-cache`) | US-WA-1 | 삭제 — 해당 리전에 대상 GPU가 없어 사용 불가 |
| `n9ja6mqq4f` (`qwen3.8-27b-cache-euro1`) | EU-RO-1 | 유지 — RTX PRO 6000 반복 실행용 |

스토리지 비용은 `USD 7.00/month`에서 `USD 3.50/month`로 줄었다.
삭제는 `infra/runpod/delete_network_volume.py`로 수행하며 `--confirm-delete` 없이는 미리보기만 출력한다.
로컬 Docker volume이 원본이고 원격 볼륨은 복제본이므로, 삭제 방향은 항상 원격으로 제한한다.

#### H200 ×1 실행 개시

| 항목 | 값 |
|---|---|
| 모델 | `Qwen/Qwen3.8-27B-FP8` (29GB, 임계값 미만이므로 Pod 직접 다운로드 허용) |
| GPU / 단가 | NVIDIA H200 ×1 / USD 4.59/hour |
| 가능 리전 | US-CA-2, US-GA-2, US-NC-1 (재고 `Low`) |
| Network volume | 미연결 — EU-RO-1 볼륨은 H200 리전과 무관 |
| Case | 8건 + smoke test |
| 비용 상한 | USD 4.59 |

EU-RO-1 볼륨을 H200에 재사용할 수 없는 이유는 H200이 EU-RO-1에 없기 때문이다.
볼륨은 리전을 고정하므로 GPU별로 캐시 리전이 달라진다.

### 2026-08-28 — Pod 실행 5: H200 ×1 측정 성공 (단, KV dtype 폴백)

| 항목 | 결과 |
|---|---|
| Pod | `9ytm3dmnfc8ogn` |
| 측정 case | 8/8 성공, 실패 요청 0 |
| 실행시간 / 비용 | 1,125.19초 / USD 1.4346 |
| 판정 | `MEASURED (FP8 폴백)` |

#### 기동 시도별 결과

| 시도 | 요청 KV dtype | 선택된 backend | KV cache tokens | 최대 동시성 | 결과 |
|---:|---|---|---:|---:|---|
| 1 | fp8 | FLASH_ATTN | 2,885,090 | 11.01x | 실패 |
| 2 | fp8 | FLASH_ATTN | 2,877,557 | 10.98x | 실패 |
| 3 | auto (BF16) | FLASH_ATTN | 1,472,157 | 5.62x | 성공 |

실패 원인은 FP8 KV cache 경로가 요구하는 cubin 부재다.

```text
RuntimeError: Assertion failed: !cubin.empty() || isPathValid(path_)
```

#### 내 폴백 구현의 결함 — 백엔드가 바뀌지 않았다

세 시도 모두 `ATTENTION_BACKEND` 환경변수로 `TRITON_ATTN`을 지정했으나, 로그의 실제 선택은 전부 `FLASH_ATTN`이었다.
vLLM 0.28.0은 `VLLM_ATTENTION_BACKEND` 환경변수를 반영하지 않고 `--attention-backend` CLI 인자를 사용한다.
따라서 "백엔드 폴백"은 같은 백엔드를 세 번 시험한 것이었고, 실제로 기동을 성공시킨 변수는 **KV cache dtype을 fp8에서 auto로 낮춘 것**뿐이다.

수정: `start_vllm.sh`가 `--attention-backend` 인자를 전달하도록 바꿨다.
폴백 순서도 비교 가능성을 위해 `FLASHINFER + fp8` → `자동 backend + fp8` → `자동 backend + BF16` 으로 재정렬했다.

#### 비교 가능성 경고

RTX PRO 6000은 FP8 KV cache, H200은 BF16 KV cache로 측정됐다.
따라서 **KV cache 용량과 최대 동시성은 직접 비교할 수 없다.**
성공한 H200 실행의 1,472,157 tokens가 RTX PRO 6000의 1,615,048 tokens보다 작은 것은 dtype 차이 때문이지 H200이 열등해서가 아니다.
실패한 시도의 프로파일링 값에서 H200이 FP8로 기동했다면 약 2,885,090 tokens / 11.01x였을 것임을 확인할 수 있다.

#### 측정값

| Context | 동접 | 성공 | TTFT p50 (s) | TTFT p99 (s) | ITL p50 (ms) | Decode tok/s | Aggregate tok/s |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 32,768 | 1 | 1/1 | 2.35 | 2.35 | 11.6 | 33.67 | 8,615 |
| 32,768 | 2 | 2/2 | 1.31 | 2.40 | 12.6 | 63.99 | 16,372 |
| 32,768 | 4 | 4/4 | 3.29 | 7.30 | 13.5 | 56.76 | 14,523 |
| 32,768 | 8 | 8/8 | 2.18 | 9.15 | 15.6 | 91.86 | 23,504 |
| 32,768 | 16 | 16/16 | 3.89 | 18.28 | 20.2 | 98.42 | 25,183 |
| 65,536 | 1 | 1/1 | 3.12 | 3.12 | 12.1 | 27.71 | 14,182 |
| 131,072 | 1 | 1/1 | 8.18 | 8.18 | 13.1 | 13.10 | 13,412 |
| 262,144 | 1 | 1/1 | 24.43 | 24.43 | 15.0 | 4.89 | 10,009 |

peak VRAM 128,893 / 143,771 MiB, 평균 GPU 사용률 45.6%.

#### 판정

`Conditionally Recommended`. 사용자당 decode 속도는 RTX PRO 6000의 약 1.8~2.2배이고 64K 단일 사용자까지 SLA를 만족한다.
32K 기준 SLA 충족 동접은 두 구성 모두 2명이며, H200 단가가 2.2배이므로 동접당 비용 효율은 유사하다.
결과는 `docs/hardware-sizing.md`에 통합돼 있다.

#### 누적 비용

컴퓨트 누적 `USD 6.5138`, 그중 실측값 생산 `USD 1.8782`.

### 2026-08-28 — H200 FP8 재측정 성공 및 Phase 4 비교표

#### 진짜 원인은 nvcc 경로였다 (앞선 진단 정정)

FP8 실패 원인을 CUDA 버전 불일치로 추정했으나 틀렸다.
성공한 실행이 사용한 nvcc는 여전히 `/usr/local/cuda`의 12.9였다.

| 항목 | 값 |
|---|---|
| CUDA_HOME | `/usr/local/cuda` |
| nvcc | `/usr/local/cuda/bin/nvcc`, release 12.9 |

실제 원인은 **서버 프로세스의 PATH에 nvcc가 없었던 것**이다.
DeepGEMM은 FP8 GEMM 커널을 JIT 컴파일하는데, nvcc를 실행하지 못해 cubin이 생성되지 않았고 그 결과가 `!cubin.empty() || isPathValid(path_)` 어서션이었다.
`start_vllm.sh`가 nvcc 디렉터리를 PATH에 넣도록 고치자 FP8이 1회차에 기동했다.

버전이 문제였다면 12.9로 성공할 수 없었으므로, 이 정정은 실측으로 확인된 것이다.

#### 중간에 발생한 내 실수

nvcc 진단 코드를 `set -e` 아래에 두어, nvcc가 없을 때 스크립트가 vLLM 기동 전에 중단됐다.
FP8과 무관한 결함으로 USD 0.4135를 소모했다.
이후 가짜 `vllm` 바이너리로 로컬 검증을 거쳐 `--attention-backend` 전달과 nvcc 부재 시 비중단을 확인한 뒤 재실행했다.

#### strict 모드의 효과

`--strict-kv-cache-dtype`은 FP8로 기동하지 못하면 BF16으로 폴백하지 않고 중단한다.
FP8 재측정 목적에서 BF16 폴백은 이미 보유한 데이터를 다시 사는 것이기 때문이다.
실패한 시도는 USD 0.9304에서 멈췄다.

#### 세 GPU FP8 동일 조건 비교

| GPU | KV dtype | KV cache tokens | 최대 동시성 (262K) | peak VRAM | 단가 |
|---|---|---:|---:|---:|---:|
| RTX PRO 6000 ×1 | fp8 | 1,615,048 | 6.16x | 87,779 MiB | USD 2.09 |
| H200 ×1 | fp8 | 2,885,090 | 11.01x | 129,913 MiB | USD 4.59 |
| B200 ×1 | fp8 | 3,886,962 | 14.83x | 164,584 MiB | USD 6.79 |

사용자당 decode 속도(ITL 역수, 32K 동접 1 기준)는 44.6 / 93.1 / 102.3 tok/s다.

#### SLA 판정 정정

앞서 사용자당 decode 속도로 `output_throughput`을 동접으로 나눈 값을 썼는데 이는 잘못이다.
그 값은 prefill을 포함한 서버 전체 처리량이라 사용자당 속도가 아니다.
ITL 역수로 다시 판정한 결과 decode 속도는 모든 구간에서 기준을 크게 넘으며, 실제 제약은 TTFT다.

| GPU | 32K에서 SLA 충족 최대 동접 | 제약 요인 |
|---|---:|---|
| RTX PRO 6000 ×1 | 8 | TTFT |
| H200 ×1 | 16 (측정 범위 상한) | 미도달 |
| B200 ×1 | 8 | TTFT (동접 16 측정치 변동 의심) |

이전 보고서의 "동접 2까지"라는 판정은 잘못된 지표에 근거한 것이므로 폐기한다.

#### 산출물

| 파일 | 내용 |
|---|---|
| `results/summary.csv` | 24행 매트릭스 (지시서 17절) |
| `docs/hardware-sizing.md` | `benchmark.summarize`가 자동 생성 (지시서 26절) |
| `results/remote/<run>/` | 실행별 raw 데이터 |
| `results/recovered/` | 덮어쓰기로 소실된 RTX PRO 6000 raw의 복원본, 출처 명시 |

#### 누적 비용

| 항목 | 값 |
|---|---:|
| 컴퓨트 누적 | USD 11.5568 |
| 그중 실측값 생산 | USD 5.5773 |
| 스토리지 | USD 3.50/month |

### 2026-08-28 — 코드 에이전트 workload 측정 (지시서 13절)

RTX PRO 6000 ×1, FP8, 프롬프트 28,497~28,500 토큰. 자연 길이와 강제 길이를 각각 측정했다.

#### 강제 길이 (통제된 decode)

| 출력 | 동접 | 생성 | TTFT p50 | ITL p50 | 사용자당 tok/s | 서버 tok/s |
|---:|---:|---:|---:|---:|---:|---:|
| 512 | 1 | 512 | 3.606s | 22.06ms | 45.3 | 34.4 |
| 512 | 4 | 2,048 | 0.313s | 24.51ms | 40.8 | 159.9 |
| 2,048 | 1 | 2,048 | 0.135s | 22.00ms | 45.5 | 45.4 |
| 2,048 | 4 | 8,192 | 0.396s | 24.42ms | 41.0 | 162.8 |
| 8,192 | 1 | 8,192 | 0.163s | 22.04ms | 45.4 | 45.3 |
| 8,192 | 4 | 32,768 | 0.369s | 24.67ms | 40.5 | 161.9 |

#### 자연 길이에서 드러난 측정 결함

동접 1에서 모델은 이 과제를 71토큰으로 답하고 멈췄다.
따라서 상한만 2,048/8,192로 두면 그 길이는 실제로 측정되지 않는다.
`ignore_eos`와 `min_tokens`로 정확한 길이를 강제해 다시 측정했고, 두 실행을 모두 보존한다.

#### 관찰

prefix cache가 TTFT를 3.606초에서 0.135~0.396초로 줄인다.
repository prefix가 공유되므로 최초 prefill 이후 캐시에 적중하며, 에이전트의 두 번째 턴부터는 사실상 즉시 시작된다.

사용자당 decode 속도는 출력 길이와 무관하게 45.3~45.5 tok/s로 일정하다.
동접 4에서 사용자당 40.5 tok/s로 11%만 떨어지는 대신 서버 처리량은 3.6배가 된다.

판정 `Recommended` (코드 에이전트 용도, 동접 4까지).
결과는 `docs/hardware-sizing.md`의 코드 에이전트 절에 통합돼 있다.

### 2026-08-28 — 실행 시간 구조 개선

성공한 실행의 72~83%가 측정이 아니라 준비 과정이었다.

| 실행 | 전체 | 벤치마크 | 오버헤드 |
|---|---:|---:|---:|
| B200 매트릭스 | 1,424s | 244s | 1,181s (83%) |
| H200 FP8 | 794s | 167s | 627s (79%) |
| 코드 에이전트 | 988s | 275s | 713s (72%) |

원인은 세 가지였고 모두 회피 가능했다.

| 원인 | 손실 | 조치 |
|---|---|---|
| vLLM을 매 실행 pip 설치 | ~300초/실행 | 공식 `vllm/vllm-openai` 이미지로 부팅. sshd만 기동 시 설치 |
| 모델을 매 실행 다운로드 | ~120초/실행 | EU-RO-1 볼륨 마운트 |
| 실행을 순차로만 수행 | wall clock 3배 | 실행 상태를 `results/runs/<name>/`로 분리해 병렬 실행 |

공식 이미지를 초기에 배제한 판단이 틀렸다.
sshd가 없다는 이유였는데, sshd는 부팅 시 수십 초면 설치된다.
그 수십 초를 아끼려고 매번 5분짜리 설치를 반복했다.

지출 요율 상한 USD 80/hour 대비 세 GPU 동시 실행은 USD 13.47로 17%에 불과하므로, 순차 실행은 한도가 아니라 내가 만든 단일 상태 파일 때문이었다.

#### 예상 시간과 능동 감시

실행 전에 예상 소요 시간을 산출하고, 초과하면 즉시 확인한다.

| 단계 | 예산 |
|---|---:|
| Pod 부팅 + SSH | 180s |
| 프로젝트 복사 | 60s |
| vLLM 설치 | 420s (이미지 사용 시 0s) |
| 모델 준비 + 서버 기동 | 480s |
| 벤치마크 | 900s |

`scripts/watch_runs.py`가 실행별 경과시간과 누적 비용을 감시하고 예산 초과 시 보고한다.
죽은 서버를 타임아웃까지 지켜보며 27분을 과금한 사례가 이 감시가 필요한 이유다.

#### 캐시 볼륨의 대가

볼륨은 리전을 고정한다.
RTX PRO 6000은 볼륨을 붙이면서 사용 가능 리전이 5곳에서 EU-RO-1 한 곳으로 줄었고, 병렬 실행 시도에서 실제로 재고를 잡지 못했다.

29GB 모델에서 캐시가 아끼는 시간은 약 120초인데, 리전 선택지를 5분의 1로 줄이는 대가가 그보다 크다.
따라서 Qwen 규모에서는 볼륨 없이 리전 폭을 확보하는 편이 낫고, 700GB급 GLM/DeepSeek에서는 반대로 볼륨이 필수다.

### 2026-08-28 — 공식 vLLM 이미지 채택과 검증되지 않았던 전제

#### 초기 판단이 틀렸다

공식 `vllm/vllm-openai` 이미지를 초기에 배제한 근거는 "이 이미지에는 sshd가 없어서 SSH 기반 오케스트레이션을 쓸 수 없다"였다.
이 전제를 한 번도 검증하지 않았고, 그 대가로 매 실행 약 300초의 pip 설치를 반복했다.

단일 Pod 검증 결과는 다음과 같다.

| 항목 | 실측 |
|---|---:|
| 생성 → 컨테이너 기동 (10GB+ 이미지 pull 포함) | 123.8초 |
| 컨테이너 기동 → SSH 접속 성공 | 2.6초 |
| 이미지의 vLLM | 0.28.0+cu129 확인 |
| 검증 비용 | USD 0.0834 |

SSH가 컨테이너 기동 2.6초 후 붙었으므로 `apt-get install openssh-server`는 실행되지 않았다.
즉 sshd 바이너리는 이미 이미지에 있었다.
다만 sshd를 **실행**시키는 것은 여전히 필요하다. 이미지의 기본 ENTRYPOINT는 `["vllm","serve"]`이므로 아무도 sshd를 띄우지 않기 때문이다.

이 결론은 SSH 성립 시간에서 추론한 것이다. 부트스트랩이 어느 분기를 탔는지 Pod에 기록하도록 수정해 다음 실행에서 관측으로 확정한다.

#### 세 번 반복한 실패 — ENTRYPOINT

| 시도 | 설정 | 결과 |
|---|---|---|
| 1 | `dockerStartCmd`만 지정 | 이미지 ENTRYPOINT가 살아 있어 명령이 `vllm serve`의 인자가 됨. SSH 거부 |
| 2 | `dockerEntrypoint: []` 추가 | RunPod가 빈 리스트를 `null`로 저장. 동일 실패 |
| 3 | `dockerEntrypoint: ["/bin/bash","-lc"]` | PASS |

빈 리스트로는 ENTRYPOINT가 지워지지 않는다. 실제 인터프리터로 교체해야 한다.
이 실패로 Pod 6개, 약 USD 2.04를 소모했다.
`scripts/verify_pod_boot.py`는 Pod 하나로 SSH 접속까지만 확인하고 종료하며, 비용은 약 USD 0.08이다.
이미지나 기동 명령을 바꿀 때는 벤치마크 전체를 돌리기 전에 이 검증을 먼저 수행한다.

#### 도커 이미지 캐싱

문서상 Pod가 stop되면 컨테이너 디스크가 삭제되며, 사용자가 제어할 수 있는 이미지 캐시나 pre-pull 기능은 없다.
Network volume에는 컨테이너 이미지를 담을 수 없다.

다만 실측 pull 시간이 기존 `runpod/pytorch` 부팅 시간과 비슷하므로 이미지 교체는 순이득이다.

| 방식 | 부팅 | 설치 |
|---|---:|---:|
| `runpod/pytorch` + pip | ~120초 | ~300초 |
| `vllm/vllm-openai` | 124초 | 0초 |

#### 감시 도구 자체의 결함

단계별 감시를 도입한 뒤 감시기에서 세 가지 결함을 연달아 발견했다.

| 결함 | 영향 | 수정 |
|---|---|---|
| 전체 상한만 검사 | SSH 실패를 900초 뒤에야 인지 | 단계별 예산으로 분리 |
| 죽은 오케스트레이터의 마지막 상태를 활성으로 간주 | 이미 중단된 실행을 SLOW로 오보 | 실제 Pod 상태 대조 |
| API 조회 실패를 "실행 없음"으로 해석 | 살아 있는 실행 전부를 stale 처리하고 감시 종료 | 조회 실패 시 생존 확인만 건너뜀 |

세 번째가 가장 위험하다. 감시가 필요한 순간에 조용히 꺼지는 방향으로 실패하기 때문이다.
수정 직후 실제로 `RunPodError`가 발생했고, 이번에는 해당 사이클만 건너뛰고 감시를 계속했다.

### 2026-08-29 — GLM 캐시 준비와 다운로드 전용 인스턴스

#### 모델 실측 크기

추정치를 Hugging Face API 실측으로 교체했다.

| 모델 | 설정상 추정 | 실측 | shards |
|---|---:|---:|---:|
| GLM-5.3-Flash | 700GB | **328.4GB** | 62 |
| DeepSeek-V3.2 | 690GB | 689.5GB | 163 |
| Qwen3.8-27B-FP8 | 29GB | 30.9GB | 66 |

GLM 추정치가 두 배 과다했다. 볼륨 크기와 실행 예산이 모두 이 값에 의존하므로 추정 대신 조회한다.

#### 다운로드에 GPU를 쓰지 않는다

바이트를 복사하는 데 가속기는 필요 없다.
초기 구현에는 "CPU Pod 실패 시 최저가 GPU로 대체"라는 경로가 있었고, CPU 페이로드 오류로 실제로 H100(USD 3.29/hour)이 생성됐다.
이 대체 경로를 코드에서 완전히 삭제했다.

| 항목 | 값 |
|---|---|
| 사용 인스턴스 | CPU Pod 2 vCPU, USD 0.06/hour |
| 스키마 | `computeType: "CPU"` + `vcpuCount`. `instanceIds`는 존재하지 않는 필드다 |
| 큰 vCPU | 재고가 없는 경우가 많아 사다리로 낮춰 시도한다 |
| 성능 요구 | 없음. 전송은 네트워크 바운드이므로 최저 사양이 정답이다 |

#### S3와 마운트의 역할 구분

| 경로 | 용도 | 속도 |
|---|---|---|
| 로컬 → S3 API | 이미 로컬에 받아둔 모델을 올릴 때 | 가정용 업링크 12.3 MiB/s, 329GB에 7.1시간 |
| Pod에서 HF → 마운트된 볼륨 | 새 모델을 채울 때 | 실측 250 MB/s, 329GB에 약 22분 |

새 모델을 채울 때 S3를 경유할 이유가 없다. 볼륨을 마운트한 Pod가 직접 쓰는 편이 빠르고 단순하다.

#### 캐시 채우기에서 발생한 결함

| 실패 | 비용 | 원인 |
|---|---:|---|
| GPU 대체 경로 작동 | USD 0.09 | CPU 페이로드 스키마 오류 |
| 원격 명령이 첫 단어만 실행 | USD 0.0006 | `run_remote_shell` 대신 raw `run` 사용 |
| 모듈을 찾지 못함 | USD 0.0006 | `huggingface_hub.commands`가 최신 버전에서 제거됨 |
| OOM kill (exit 137) | USD 0.0011 | 4GB RAM에 `max_workers=8` |

두 번째가 이 프로젝트 최초 실패와 같은 원인이다.
전용 함수 `run_remote_shell`을 만들어 두고도 새 스크립트에서 쓰지 않아 같은 버그를 재생산했다.
고친 것을 쓰지 않으면 고친 것이 아니다.

GPU 사용 금지 덕분에 이후 세 번의 실패 비용 합계가 1센트 미만이었다.

#### hf_transfer는 더 이상 쓰이지 않는다

`HF_HUB_ENABLE_HF_TRANSFER`는 deprecated이며 Hugging Face는 Xet 백엔드로 전환했다.
이 변수에 근거해 세웠던 9분 예상은 무효이고, 실측 250 MB/s 기준 약 22분이 맞다.

#### 진행 감시

`scripts/watch_download.sh`가 90초 간격으로 누적 용량, 진행률, 실측 속도, ETA를 보고한다.
진행이 멈추면 `STALLED`, 호스트가 응답하지 않으면 3회 실패 후 종료한다.
장시간 전송은 정상 동작과 정지가 겉보기에 같으므로 속도를 명시적으로 보고해야 한다.

#### 진행 감시 방식

장시간 작업은 다음 순서로 관찰한다.

1. 작업 자체의 출력을 스트리밍한다. 원격 명령의 stdout을 로컬 파일에 기록하고 tail한다.
2. 스트리밍이 불가능하면 30초 간격으로 외부에서 조회한다.

두 경우 모두 백분율만 출력하지 않고 실측 속도와 ETA를 함께 낸다.
정지한 전송과 정상 전송은 속도 없이는 구분되지 않는다.

| 도구 | 방식 | 간격 |
|---|---|---:|
| `fill_cache_volume.py --progress-log` | 전송 출력 스트리밍 | 즉시 |
| `watch_download.sh` | 용량 조회 + 속도/ETA 산출 | 30초 |
| `watch_runs.py` | 실행 단계 조회 + 예산 초과 경보 | 30초 |

#### GPU 리전과 볼륨 리전이 다를 때

볼륨은 Pod를 자기 리전에 고정하므로, 대상 GPU가 다른 리전에만 있으면 그 리전에 볼륨이 필요하다.
이때 기존 볼륨을 복사할 것인지 새로 받을 것인지는 다음과 같이 판단한다.

| 방법 | 가능 여부 | 판정 |
|---|---|---|
| S3 → S3 직접 복사 | 불가 | 엔드포인트가 리전별로 분리되어 서버 사이드 복사가 없다. 로컬 경유는 업링크 12.3 MiB/s로 329GB에 7.1시간 |
| Pod ↔ Pod 전송 | 가능 | 양쪽 리전에 Pod가 필요해 비용이 두 배이고 리전 간 대역폭에 종속된다 |
| Hugging Face에서 재다운로드 | 가능 | 실측 670~800 MB/s. 329GB에 약 8분, CPU Pod 하나로 USD 0.01 |

**원칙: 볼륨은 복제 대상이 아니라 재생성 대상이다.**
공개 체크포인트라면 어느 리전에서든 HF에서 다시 받는 편이 빠르고 싸다.

복사가 필요한 경우는 재다운로드가 불가능하거나 원본이 HF에 없을 때로 한정된다.

* gated 또는 private 저장소여서 토큰과 승인이 필요한 경우
* 로컬에서 변형한 체크포인트(양자화, 머지)라 원본이 존재하지 않는 경우
* HF에서 삭제되었거나 변경되어 특정 revision을 보존해야 하는 경우

이 경우에도 Pod 간 전송보다 로컬 원본을 S3로 올리는 편이 단순하다. 다만 업링크 속도 때문에 대용량에는 적합하지 않다.

### 2026-08-29 — 클린업 절차

RunPod에서 비용은 두 가지 경로로 조용히 누적된다.

| 자원 | 과금 방식 | 방치 시 증상 |
|---|---|---|
| Pod | 실행 중 초 단위 | 오케스트레이터가 죽으면 stop되지 않고 계속 과금된다 |
| Pod 디스크 | 정지 상태에서도 유지 | 실행이 끝나도 컨테이너 디스크와 볼륨이 남는다 |
| Network volume | 월 단위, 사용 여부 무관 | 쓸 수 없는 리전의 볼륨도 매달 청구된다 |

실제로 이 프로젝트에서 유휴 Pod 10개가 2,300GB를 붙들고 있었고, 대상 GPU가 없는 리전의 볼륨이 월 USD 3.50을 소모했다.

#### 도구

```bash
python3 scripts/cleanup_resources.py                          # 조회만, 항상 안전
python3 scripts/cleanup_resources.py --terminate-idle --confirm
python3 scripts/cleanup_resources.py --stop-running --confirm
python3 scripts/cleanup_resources.py --delete-volumes VOL_ID --confirm
```

조회는 무료이며 부작용이 없다. 파괴적 동작은 `--confirm` 없이는 수행되지 않고, `--keep`으로 보존할 자원을 지정한다.
`--stop-running`을 명시하지 않는 한 실행 중인 Pod는 건드리지 않는다.

#### 표준 절차

##### 1. 매 실행 종료 후

오케스트레이터는 `finally`에서 Pod를 stop한다. 오케스트레이터가 비정상 종료되면 이것이 실행되지 않으므로 반드시 확인한다.

```bash
python3 scripts/cleanup_resources.py
```

`burn_usd_per_hour`가 0이 아니면 의도한 실행인지 확인하고, 아니면 중단한다.

##### 2. 측정 세션이 끝난 뒤

정지된 Pod는 컴퓨트 과금은 없으나 디스크를 계속 점유한다. 결과를 회수한 뒤 삭제한다.

```bash
python3 scripts/cleanup_resources.py --terminate-idle --confirm
```

결과 회수가 먼저다. `results/runs/<name>/remote/`에 아티팩트가 있는지 확인한 뒤 삭제한다.

##### 3. 모델 측정이 끝난 뒤

볼륨은 월 단위로 계속 청구된다. 해당 모델을 다시 측정할 계획이 없으면 삭제한다.

```bash
python3 scripts/cleanup_resources.py --delete-volumes VOL_ID --confirm
```

볼륨은 복제본이므로 삭제해도 복구 가능하다. 공개 체크포인트는 Hugging Face에서 약 8분에 다시 받을 수 있고, 그 비용은 CPU Pod USD 0.01이다.
월 USD 28짜리 400GB 볼륨을 놀리는 것보다 필요할 때 다시 만드는 편이 싸다.

##### 4. 로컬

로컬 Docker volume은 원본이므로 삭제하지 않는다. 삭제 방향은 항상 원격으로 제한한다.

#### 주의

`pkill -f` 로 오케스트레이터를 종료하면 패턴이 자기 셸에도 일치해 부모 프로세스까지 죽는다.
이 경우 Pod를 stop하는 `finally`가 실행되지 않아 Pod가 고아로 남는다.
PID를 확인해 개별 종료하고, 종료 후에는 반드시 위 1번으로 실제 Pod 상태를 확인한다.

### 2026-08-29 — 코드 리뷰 반영

GLM 실행 직전 코드 리뷰에서 12건이 지적됐고, 이번 실행에 영향이 있는 항목을 수정했다.

| 결함 | 영향 |
|---|---|
| `wait_for_server.sh`에 `TIMEOUT_SECONDS` 미전달 | 원격은 1800초, 로컬은 900초에 끊겨 `TimeoutExpired`가 루프 밖으로 나갔다. **백엔드 폴백 2·3차가 도달 불가능**이었다 |
| `finally`의 `float(state["gpu_hourly_cost"])` | 요율이 없으면 TypeError가 나며 원래 예외와 lifecycle 기록을 함께 잃었다 |
| `copy_project`의 `scp -r` 중첩 | 대상 디렉터리가 있으면 `scripts/scripts`로 중첩돼 재개한 Pod가 옛 코드를 실행했다 |
| `verify_pod_boot`의 probe 재할당 | 볼륨 검사 결과로 판정해 vLLM이 없는 이미지도 PASS로 보고했다 |
| vCPU 사다리가 오름차순 | `--vcpu 8`이 2를 먼저 시도해 요청한 크기를 건너뛰었다 |
| venv 버전 비교에 `cut -d+` 누락 | 이미지 경로에만 적용해 venv는 매 실행 재빌드됐다 |
| `monitor_gpu.sh`의 `set -e` | `nvidia-smi` 일시 실패로 감시가 종료되는데 헤더 때문에 파일이 비지 않아 아티팩트 검사에 걸리지 않았다 |
| `summarize.py`의 TypeError/IndexError | startup 로그가 없거나 실행이 0건이면 `summary.csv`를 먼저 지운 뒤 예외를 던졌다 |
| code-agent fail-fast 하드코딩 | `concurrency==1 and output_tokens==512`에 고정돼 다른 조합으로 시작하면 전 케이스 실패를 끝까지 돌았다 |
| SSH `ConnectTimeout` 누락 | 연결이 매달리면 재시도 루프 밖으로 예외가 나갔다 |

첫 번째가 가장 심각하다. 폴백 체인을 만들어 두고 타임아웃 경로에서는 한 번도 동작하지 않았다.
H200 FP8이 폴백으로 성공했던 것은 엔진이 죽어 exit 2가 났기 때문이고, 타임아웃이었다면 폴백 없이 실패했을 것이다.

### 2026-08-29 — 리전 제약의 실체

세 가지 제약이 서로 다른 집합이라는 점이 이번에 분명해졌다.

| 집합 | 내용 |
|---|---|
| GPU 재고가 있는 리전 | 시점에 따라 변한다 |
| Pods API가 허용하는 리전 | 28곳 고정. 재고 조회에는 나오지만 생성이 거부되는 리전이 있다 |
| 네트워크 볼륨이 가능한 리전 | 13곳. `US-GA-2`와 `US-NC-1`은 H200 ×4를 제공하지만 볼륨을 만들 수 없다 |

네트워크 볼륨은 리전에 묶이며 같은 데이터센터의 Pod만 마운트할 수 있다. S3 API는 어디서든 접근되지만 마운트는 아니다.
따라서 "모든 후보 리전에 볼륨을 미리 만들어 둔다"는 전략은 성립하지 않는다. 볼륨을 만들 수 없는 리전이 존재하기 때문이다.

| 상황 | 대응 |
|---|---|
| 볼륨 가능 리전에 재고 | 볼륨 사용, 다운로드 0초 |
| 볼륨 불가 리전에 재고 | `--allow-pod-download`로 Pod가 직접 받는다. 약 8분, USD 2.45 |
| 재고 없음 | 조회는 무료이므로 대기한다 |

희소한 구성에서는 볼륨을 아끼려다 슬롯을 놓치는 편이 더 비싸다.
리전 목록은 `data_center_ids()`에서 Pods API enum과 교집합을 취해 걸러낸다. 목록에 허용되지 않는 리전이 하나만 있어도 생성 전체가 실패한다.

### 2026-08-29 — GLM-5.3-Flash: vLLM 미지원으로 측정 불가

| 항목 | 결과 |
|---|---|
| Pod | `edl0sa8rwj58hs`, H200 ×4, US-GA-2, USD 18.36/hour |
| 실행시간 / 비용 | 326.05초 / USD 1.6628 |
| 측정 case | 0건 |
| 판정 | `MODEL UNSUPPORTED — INFRASTRUCTURE PASS` |

#### 원인

```text
ValidationError: 1 validation error for ModelConfig
Value error, The checkpoint you are trying to load has model type `glm5_next`
but Transformers does not recognize this architecture.
```

vLLM 0.28.0에 포함된 Transformers가 `glm5_next` 아키텍처를 알지 못한다.
모델 config 검증 단계에서 중단되었으므로 329GB 다운로드는 시작되지도 않았다.

#### 지원 현황 조회

| 항목 | 상태 |
|---|---|
| PR #53906 `[Model] add GLM-5.3-Flash support` | **open, 미병합** (2026-08-26 생성) |
| #54062 | `Glm5NextTextLinearAttention` 아키텍처 미지원 |
| #53963 | SM120에서 rope-free MLA 경로 부재 |
| #54150 | NVFP4 체크포인트가 잘못된 UTF-8 토큰 생성 |

병합되지 않았으므로 릴리스는 물론 main에도 반영되어 있지 않다.
PR 브랜치를 직접 빌드하더라도 위 미해결 버그들 때문에 유효한 측정을 얻기 어렵고, 지시서 3절의 stable 우선 원칙에도 어긋난다.

따라서 **GLM-5.3-Flash는 현재 어떤 vLLM 버전으로도 측정할 수 없다.**

#### 인프라는 정상 동작했다

이 실행에서 인프라 요소는 모두 통과했다.

| 요소 | 결과 |
|---|---|
| H200 ×4 Pod 확보 (US-GA-2) | PASS |
| sshd 기동 및 SSH 접속 | PASS |
| 이미지의 vLLM 사용 (설치 0초) | PASS |
| FP8 strict 모드 | PASS — BF16 폴백 없이 중단 |
| 프로세스 사망 감지 | PASS — 타임아웃 1,500초 대신 즉시 중단 |
| 자동 stop 및 아티팩트 회수 | PASS |

사망 감지와 strict 모드가 없었다면 같은 오류로 1,500초씩 세 번 대기해 약 USD 7.65를 소모했을 것이다.
실제 소모는 USD 1.6628이며 예산 USD 15의 11%에서 멈췄다.

#### Phase 3 현황

| 모델 | 차단 요인 |
|---|---|
| GLM-5.3-Flash | vLLM이 `glm5_next`를 지원하지 않는다 |
| DeepSeek-V3.2 | H200 ×8이 모든 리전에서 재고 0 |

**Phase 3은 현재 실행 가능한 대상이 없다.**
GLM은 vLLM PR #53906 병합 후 재시도하고, DeepSeek는 H200 ×8 재고가 회복될 때 재시도한다.

#### 자원 정리

| 자원 | 조치 |
|---|---|
| Pod `edl0sa8rwj58hs` | terminate, 600GB 회수 |
| 볼륨 `28iw9defei` (AP-JP-1, 329GB 적재) | 삭제 |
| 볼륨 `c3a5gcf54x` (US-CA-2, 비어 있음) | 삭제 |

두 볼륨 합계 월 USD 56이 발생하고 있었으나 지원 재개 시점이 불명확하므로 보존하지 않는다.
재생성은 8분에 USD 0.01이므로 유지보다 재생성이 싸다.

정리 후 실행 중 Pod 0개, 저장 비용 USD 0.00/month.

#### 누적 비용

| 항목 | 값 |
|---|---:|
| 컴퓨트 누적 | USD 16.3482 |
| 그중 실측값 생산 | USD 6.9528 |
| 저장 | USD 0.00/month |

### 2026-08-29 — Phase 1/2 종료 및 최종 보고서

#### 스택 동등성 확인

매트릭스는 `runpod/pytorch` + pip 설치 vLLM으로, 이후 실행은 공식 `vllm/vllm-openai` 이미지로 측정했다.
서로 다른 스택의 값을 한 표에 두는 것이 타당한지 H200에서 동일 조건으로 비교했다.

| 컨텍스트 | 동접 | pip 스택 | 이미지 스택 | 차이 |
|---:|---:|---:|---:|---:|
| 32,768 | 1 | 91.8 | 93.1 | +1.4% |
| 32,768 | 2 | 88.1 | 87.7 | -0.5% |
| 32,768 | 4 | 81.7 | 80.9 | -1.0% |
| 32,768 | 8 | 73.1 | 70.0 | -4.2% |
| 32,768 | 16 | 61.4 | 59.1 | -3.7% |
| 65,536 | 1 | 92.9 | 90.6 | -2.5% |
| 131,072 | 1 | 87.6 | 85.4 | -2.5% |
| 262,144 | 1 | 78.5 | 77.1 | -1.8% |

최대 4.2%, 대부분 2% 이내다. 두 스택의 측정값은 동일하게 취급할 수 있으며 매트릭스 재측정은 불필요하다.

`configs/hardware/*.yaml`의 `image_name`은 현재 도달하지 않는 설정이다.
`build_payload`가 모델 설정의 `vllm_image`를 우선하고 모든 모델이 이를 정의하기 때문이다.
매트릭스 측정 시점에는 이 우선순위가 없었으므로 하드웨어 설정의 이미지가 실제로 사용됐다.

#### 최종 보고서

Phase 1/2 결과는 `docs/hardware-sizing.md` 하나에 정리했다. `benchmark.summarize`가 실측에서 생성하므로 손으로 쓴 별도 보고서는 두지 않는다.

| 항목 | 결과 |
|---|---|
| 측정 조합 | 3 GPU × 8 case + 코드 에이전트 12 case |
| SLA 충족 최대 동접 (32K) | RTX PRO 6000 8, H200 16(상한), B200 8 |
| 실제 제약 | 전부 TTFT. decode 속도는 어느 구간에서도 병목이 아니었다 |
| prefix cache 효과 | TTFT 3.606초 → 0.135초 (24배) |
| 컴퓨트 누적 / 유효 측정 | USD 16.35 / USD 6.95 |

#### Phase 3 미완

| 모델 | 차단 요인 |
|---|---|
| GLM-5.3-Flash | vLLM이 `glm5_next`를 지원하지 않는다. PR #53906 미병합 |
| DeepSeek-V3.2 | H200 ×8이 모든 리전에서 재고 0 |

#### 남은 기술 부채

`infra/runpod/`의 663줄은 RunPod 공식 SDK와 MCP 도구의 재구현이다.
이 계층에서 스키마를 추측하다 발생한 사고가 이번 세션 실패의 상당 부분을 차지한다.

| 사고 | 원인 |
|---|---|
| GPU Pod로 모델 다운로드 | CPU Pod 페이로드에 존재하지 않는 `instanceIds` 필드 사용 |
| 스키마 탐색 중 Pod 실수 생성 | 오류 메시지를 보려고 생성 API를 호출 |
| 리전 enum 불일치 실패 3회 | 허용 목록을 문서가 아니라 오류 메시지로 학습 |
| 볼륨 생성·삭제의 HTTP 500 오해 | 성공인데 예외로 처리 |

RunPod MCP 서버는 인증됐으나 세션 시작 이후여서 이 세션에는 도구가 로드되지 않았다.
다음 세션에서 MCP 도구를 사용해 `infra/runpod/`를 교체한다. 추측으로 재작성하지 않는다.

## 2026-08-29 — 단일 스크립트 전환 후 MCP 선기동 방식 시리즈 재측정

통합 CLI(`benchmark/cli.py`)와 RunPod MCP 제어, 선기동 템플릿 방식으로
Tier 1 Qwen3.8-27B의 3개 GPU 구성을 재측정했다. 이 세션의 절차는
지시서 8절의 표준 실행 절차로 문서화했다.

### 방법 변화

| 항목 | 내용 |
|---|---|
| 진입점 | `python -m benchmark <subcommand>` 단일 스크립트로 통합 |
| 서버 기동 | 컨테이너 부팅과 함께 `vllm serve` 선기동 + sshd 부트스트랩 (`ssh-keygen -A` 포함) |
| 모델 캐시 | CPU Pod가 볼륨에 다운로드 (`cache-fill`): 29GB 약 3분, USD 0.003 |
| 리전 선택 | Pods API 허용 ∩ S3 지원 ∩ 재고 가용성 교집합. RTX PRO 6000은 전 리전 LOW 동률이므로 S3 가능 리전(EUR-IS-1) 선택 |
| 서빙명 | 볼륨 마운트 시 `local_model_path` 등록, 벤치마크 요청명과 일치 확인 |

### 발생한 실패와 수정

| 실패 | 비용 | 원인과 수정 |
|---|---:|---|
| Pod `i4f93xims5g2u4` 크래시 루프 | USD 0.3083 | MCP/GraphQL 템플릿 생성에는 entrypoint 필드가 없어 이미지 ENTRYPOINT(`vllm serve`)가 startCmd를 인자로 흡수. `mkdir -p`의 `-p`가 vLLM argparse와 충돌. `stream-pod-logs`로 즉시 원인 파악 후 stop. **선기동 Pod는 REST v1 pod create로만 생성** (지시서 5절 규칙 표에 추가) |
| Pod `ty65krec65x5fi` | USD 0.1341 | 선기동 동작 확인 후 캐시 우선 전환으로 중단 |
| 볼륨 생성 HTTP 500 | USD 0 | 성공. 목록 재조회로 확인 (기록된 함정) |

### 측정 결과 — 8/8 case 성공, 실패 요청 0 (3개 GPU 공통)

FP8 KV cache, prefix caching, max_model_len 262,144, TP=1, output 128 tokens.

| GPU | KV cache tokens | 최대 동시성 | peak VRAM | 32K/1 TTFT | 32K/1 decode | 64K/1 TTFT | 131K/1 TTFT | 262K/1 TTFT | 262K/1 decode |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| RTX PRO 6000 ×1 | 1,612,034 | 6.15x | 87,717 MiB | 4.08s | 18.6 tok/s | 6.29s | 18.05s | 58.27s | 2.08 tok/s |
| H200 ×1 | 2,883,584 | 11.00x | 129,437 MiB | 2.67s | 31.7 tok/s | 3.95s | 10.79s | 33.33s | 3.65 tok/s |
| B200 ×1 | 3,882,443 | 14.81x | 164,554 MiB | 1.79s | 42.9 tok/s | 2.11s | 4.78s | 11.88s | 9.78 tok/s |

32K 동시성 확장 (TTFT p50 / decode tok/s 합계):

| 동접 | RTX PRO 6000 | H200 | B200 |
|---:|---|---|---|
| 2 | 2.49s / 34.7 | 1.87s / 58.8 | 3.23s / 34.1 |
| 4 | 5.38s / 43.4 | 7.45s / 47.3 | 6.31s / 36.9 |
| 8 | 4.15s / 50.0 | 4.90s / 84.3 | 5.59s / 50.2 |
| 16 | 9.02s / 53.5 | 4.30s / 91.6 | 11.74s / 41.9 |

이전 실측과 최대 몇 % 이내로 일관되며, 두 실행 스택(MCP 선기동 vs 기존)의
동등성도 재확인됐다. B200은 32K 동접 4/16에서 TTFT 변동이 다시 관측됐다
(이전 기록의 "B200 동접 재측정" 남은 과제와 동일한 현상).

### 판정

| GPU | SLA 충족 최대 동접 (32K) | 판정 |
|---|---:|---|
| RTX PRO 6000 ×1 | 8 | Conditionally Recommended |
| H200 ×1 | 16 (측정 범위 상한) | Conditionally Recommended |
| B200 ×1 | 2 (TTFT 변동으로 4·16 미충족; 이전 실측 8과 불일치) | Conditionally Recommended — TTFT 변동 해소 필요 |

제약은 전부 TTFT며 decode 속도는 모든 구간에서 기준(15 tok/s)을 넘는다.

### 비용

| 항목 | 비용 |
|---|---:|
| RTX PRO 6000 측정 (857초) | USD 0.4975 |
| H200 측정 (893초) | USD 1.1386 |
| B200 측정 (507초) | USD 0.9563 |
| 크래시 루프 / 선기동 확인 중단 | USD 0.4424 |
| CPU Pod 캐시 채우기 2회 | USD 0.0062 |
| 이번 세션 합계 | **USD 3.0410** |
| 측정값을 만든 비용 | USD 2.5924 |

### 자원 정리

| 자원 | 조치 |
|---|---|
| Pod 3개 (`pt6pv1y2d6pnak`, `likwonuw2rwsb3`, `p2s6ahn2gs1l24`) | 회수 후 stop → terminate |
| 볼륨 `vx4tzhxxmz` (EUR-IS-1), `5icinhpa12` (US-CA-2) | 유지 — USD 7.00/month. 재실행 계획에 따라 삭제 후 USD 0 |
| 원본 결과 | `results/runs/onprem-qwen3.8-27b-{rtxpro6000,h200,b200}-0829b/remote/` |
