Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ MINIO_BUCKET=docgrid
# 임베딩 서버 - application.yml에 이미 기본값(http://localhost:8000)이 있어 docker-compose 기본 포트를 쓰면 설정 불필요.
# 기본값과 다른 포트/호스트를 쓸 때만 주석 해제
# EMBEDDING_SERVER_URL=http://localhost:8000
# 문서 Batch 기본값은 실제 BGE-M3 CPU Benchmark로 선택한 32이며 환경별 재측정 시에만 조정합니다.
# EMBEDDING_DOCUMENT_BATCH_SIZE=32

# Ollama RAG LLM 서버 - application.yml에 이미 기본값(http://localhost:11434)이 있어 docker-compose 기본 포트를 쓰면 설정 불필요.
# 기본값과 다른 포트/호스트를 쓸 때만 주석 해제
Expand Down
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ tasks.register('claimPerformanceTest', Test) {
outputs.upToDateWhen { false }
}

tasks.register('bgeBatchPerformanceTest', Exec) {
group = 'verification'
description = '실제 BGE-M3 HTTP 서버의 Batch Size별 처리량, 지연과 RSS를 측정합니다.'
workingDir projectDir
commandLine(
'python3',
'embedding-server/benchmark_batch_size.py',
'--output',
layout.buildDirectory.file('reports/bge-m3-batch/bge-m3-batch-latest.json').get().asFile
)
// 실제 모델과 Docker RSS를 읽는 외부 Benchmark이므로 일반 Test와 Build Cache에서 분리한다.
outputs.upToDateWhen { false }
}

def configureOpenSqlDatabase = { Test task ->
task.maxParallelForks = 1
task.outputs.upToDateWhen { false }
Expand Down
137 changes: 137 additions & 0 deletions docs/design/gimin-#128-bge-m3-batch-size-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# 실제 BGE-M3 Batch Size 성능 비교 설계

## 1. 배경

문서 임베딩 경로는 `BAAI/bge-m3`의 Batch API와 `1~64` 범위의 설정 가능한 Batch Size를
지원한다. 현재 기본값 `16`은 기능 검증을 위해 정한 값이며, 같은 입력과 같은 실행 환경에서 수집한
성능 근거는 없다.

이번 작업은 제품 동작을 새로 추가하는 것이 아니라 실제 `/embed/batch` 경계를 반복 측정해 다음
질문에 답한다.

1. Batch Size가 커질 때 HTTP 왕복 감소와 모델 내부 Batch 처리로 처리량이 얼마나 증가하는가?
2. 처리량 증가와 요청 단위 p95·p99 지연, Embedding Server RSS 사이에 어떤 Trade-off가 있는가?
3. 로컬 CPU 개발 환경에서 문서 Batch 기본값 `16`을 유지할 근거가 있는가?

## 2. 범위

### 2.1 포함

- 실제 `POST /embed/batch` HTTP 호출
- Batch Size `1, 4, 8, 16, 32, 64`
- 같은 Text 집합과 같은 총 처리량을 사용한 순차 비교
- Warm-up과 본 측정 분리
- 처리량, 요청 지연 p50·p95·p99·max, Text당 평균 지연, 실패율 수집
- Docker 실행 시 Embedding Server PID 1의 RSS 기준선·최대값 수집
- Model명, 응답 순서·개수, 1024차원과 유한값 검증
- Host·Container·Python·Torch·Transformers 환경 지문 수집
- JSON 원본 결과와 Markdown 요약 결과 생성
- 일반 테스트와 분리된 재현 명령 제공

### 2.2 제외

- Mock Model 성능 비교
- Worker·Parsing·Vector DB를 포함한 전체 Pipeline 처리량
- 동시 HTTP 요청 부하
- GPU·CUDA·양자화 최적화
- OpenSQL 성능 측정
- Query Embedding 단건 API 변경
- BGE-M3 모델 교체

## 3. 측정 계약

### 3.1 입력 공정성

- 모든 Batch Size는 같은 순서의 결정적 한국어 Text Corpus를 처리한다.
- 한 Profile이 처리하는 Text 수는 최대 Batch Size `64`의 배수여야 한다.
- 마지막 불완전 Batch가 결과를 왜곡하지 않도록 모든 요청은 Profile의 Batch Size와 같은 개수의
Text를 포함한다.
- 기본값은 Profile당 `64`개 Text와 본 측정 `3`회다. 더 안정적인 수치가 필요하면 환경 변수로
Text 수와 반복 횟수를 높인다.
- 본 측정 Round마다 Batch Size 실행 순서를 회전해 Model Cache와 Host 열 상태가 한 Profile에만
유리하게 작용하는 편향을 줄인다.

### 3.2 Warm-up

- Health Check 성공 뒤 각 Batch Size를 한 번씩 호출한다.
- Warm-up Vector는 계약을 검증하지만 성능 통계에는 포함하지 않는다.
- Model Loading 시간은 제외한다. Cold Start는 별도 운영 지표이며 Batch Size 비교 대상이 아니다.

### 3.3 통계

각 Batch Size별로 다음 값을 기록한다.

| 지표 | 계산 |
|---|---|
| 총 Text 수 | 성공 요청이 반환한 Vector 개수 합계 |
| 처리량 | 총 Text 수 / 성공 요청 지연 합계 |
| 요청 p50·p95·p99 | 선형 보간 Percentile |
| 요청 최대 지연 | 성공 요청 중 최대값 |
| Text당 평균 지연 | 성공 요청 지연 합계 / 총 Text 수 |
| 실패율 | 실패 요청 수 / 전체 요청 수 |
| RSS 기준선·최대값 | Profile 시간 구간의 `/proc/1/status` `VmRSS` 표본 |

측정은 순차 요청으로 수행한다. 이번 결과는 Batch Size 자체의 효율을 비교하는 기준선이며 다중
Worker 병렬 부하는 후속 Worker 처리량 작업에서 다룬다.

## 4. 실행 경계

일반 `./gradlew test`는 실제 모델을 요구하지 않는다. 별도 Task가 실제 서버가 준비됐는지 확인하고
Benchmark Script를 실행한다.

```bash
docker compose up -d embedding-server
./gradlew bgeBatchPerformanceTest
```

기본 설정을 확장할 때만 다음 환경 변수를 사용한다.

```bash
BGE_BENCHMARK_TOTAL_TEXTS=256 \
BGE_BENCHMARK_ROUNDS=5 \
./gradlew bgeBatchPerformanceTest
```

원본 JSON은 `build/reports/bge-m3-batch/` 아래에 생성해 Git에 포함하지 않는다. 재현 환경, 요약 수치,
기본값 판단만 `docs/test-results/`에 기록한다.

## 5. 실패 정책

- Health Check 실패, Model명 불일치, 응답 순서·개수 불일치, 1024차원이 아닌 Vector, NaN·Infinity는
측정 실패다.
- 실패가 발생해도 완료된 Profile의 JSON 결과는 남기되 Process는 실패 Exit Code로 종료한다.
- RSS 수집은 Docker 외부 서버에서도 Benchmark를 실행할 수 있도록 선택 기능으로 둔다. Container가
없으면 성능 측정은 계속하고 RSS를 `null`로 기록한다.
- URL, Docker Container 이름 외의 접속 Secret은 입력·로그·결과에 기록하지 않는다.

## 6. 기본값 판단

측정 전에 특정 Batch Size를 정답으로 고정하지 않는다.

1. 실패율이 0이고 Vector 계약을 만족한 Profile만 비교한다.
2. 최고 처리량 Profile을 먼저 확인한다.
3. 현재 기본값 `16`이 최고 처리량의 95% 이상이면 변경하지 않는다.
4. 더 큰 Batch가 의미 있는 처리량 개선을 보이더라도 p95 지연과 RSS 증가를 함께 기록한다.
5. Apple Silicon Docker CPU 측정값은 로컬 개발 기준선으로만 사용하고 운영 SLO로 해석하지 않는다.

## 7. 검증

- Percentile·Corpus·응답 계약·요약 계산 Python 단위 테스트
- 실제 BGE-M3 Smoke 규모 Benchmark
- 실제 BGE-M3 확장 규모 Benchmark
- 기존 Python Embedding API 계약 테스트
- 전체 Java 회귀 테스트

## 8. 커밋 분할

1. `docs: #128 BGE-M3 Batch Size 성능 비교 설계 추가`
2. `test: #128 실제 BGE-M3 Batch Size Benchmark 추가`
3. `docs: #128 BGE-M3 Batch Size 측정 결과 기록`

## 9. 완료 조건

- Batch Size 6종을 같은 입력으로 한 명령에서 재현할 수 있다.
- 처리량·p50·p95·p99·Text당 지연·실패율·RSS가 구조화돼 기록된다.
- 실제 응답의 Model·개수·순서·1024차원·유한값을 측정마다 검증한다.
- 일반 테스트는 Docker나 실제 BGE-M3에 의존하지 않는다.
- 기본값 유지 또는 변경 판단이 측정 환경의 한계와 함께 문서화된다.
167 changes: 167 additions & 0 deletions docs/test-results/gimin-#128-bge-m3-batch-size-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# 실제 BGE-M3 Batch Size 성능 비교 결과

## 1. 결과 요약

2026-08-08 실제 `BAAI/bge-m3` HTTP Server에서 Batch Size `1, 4, 8, 16, 32, 64`를 같은
한국어 Text 집합으로 비교했다. 본 측정에서는 각 Profile이 256개 Text를 3회 처리해 총 768개
Vector를 생성했다.

- 전체 본 측정: 1,140 HTTP 요청, 4,608 Vector
- 실패 요청: 0
- Vector 계약 오류: 0
- Batch 32 처리량: `17.08 texts/s`
- Batch 64 처리량: `17.38 texts/s`
- Batch 32는 최고 처리량의 `98.27%`
- Batch 32 p95: `1,975.70ms`
- Batch 64 p95: `3,719.82ms`
- Batch 64의 측정 최고 RSS는 Batch 32보다 약 `83.05MiB` 높음

처리량 차이가 1.76%에 불과한 반면 Batch 64의 p95는 Batch 32의 1.88배였다. 따라서 로컬 CPU
기준 문서 임베딩 기본값을 `16`에서 `32`로 변경한다.

설계와 통계 계약은 [실제 BGE-M3 Batch Size 성능 비교 설계](../design/gimin-%23128-bge-m3-batch-size-performance.md)에
기록했다.

## 2. 실행 환경

| 항목 | 값 |
|---|---|
| Host | macOS 26.5.2, Apple Silicon arm64 |
| Host 논리 CPU | 10 |
| Embedding Container | `docgrid-embedding`, Linux |
| Python | 3.11.15 |
| Torch | 2.4.1, Thread 10 |
| Transformers | 4.44.2 |
| FlagEmbedding | 1.2.11 |
| CUDA | 사용하지 않음 |
| Model | `BAAI/bge-m3` |
| Vector 차원 | 1024 |
| Warm-up | Batch Size별 1회 |
| 본 측정 | 256 Text × 3 Round × 6 Profile |
| RSS 표본 간격 | 1초와 Profile 경계 |

이 결과는 Apple Silicon의 Docker CPU 실행 기준선이다. GPU 환경이나 공급사 운영 서버의 SLO로
사용하지 않는다.

## 3. 실행 명령

```bash
docker compose up -d embedding-server

BGE_BENCHMARK_TOTAL_TEXTS=256 \
BGE_BENCHMARK_ROUNDS=3 \
./gradlew bgeBatchPerformanceTest
```

실행 시간은 6분 47초였다. 원본 Request 표본과 환경 지문은 다음 Git 제외 경로에 생성했다.

```text
build/reports/bge-m3-batch/bge-m3-batch-latest.json
```

## 4. 측정 결과

| Batch | 요청 수 | 처리량 texts/s | p50 ms | p95 ms | p99 ms | 최대 ms | 평균 ms/Text | 최고 RSS MiB | 실패 |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | 768 | 5.85 | 166.03 | 222.97 | 254.01 | 362.37 | 170.81 | 2,176.80 | 0 |
| 4 | 192 | 11.12 | 356.99 | 409.23 | 442.71 | 459.94 | 89.93 | 2,176.80 | 0 |
| 8 | 96 | 13.57 | 585.59 | 637.61 | 681.90 | 683.95 | 73.69 | 2,176.68 | 0 |
| 16 | 48 | 15.15 | 1,027.70 | 1,224.44 | 1,498.55 | 1,543.40 | 66.02 | 2,185.02 | 0 |
| 32 | 24 | 17.08 | 1,857.51 | 1,975.70 | 1,985.38 | 1,988.18 | 58.56 | 2,176.68 | 0 |
| 64 | 12 | 17.38 | 3,681.53 | 3,719.82 | 3,726.36 | 3,728.00 | 57.55 | 2,259.72 | 0 |

모든 Profile은 기대 요청 수인 `768 / Batch Size`와 실제 요청 수가 일치했다. 각 Profile의 성공
Vector 수도 768개였고 응답 Model·Index·개수·1024차원·유한값 계약이 전 요청에서 유지됐다.

## 5. 해석

### 5.1 Batch 16에서 32

- 처리량 `15.15 → 17.08 texts/s`: `12.74%` 증가
- 평균 Text 지연 `66.02 → 58.56ms`: 감소
- p95 요청 지연 `1.22 → 1.98초`: Batch당 Text가 두 배이므로 증가
- 최고 RSS 차이는 표본 변동 범위였고 Batch 32가 더 높지 않았다.

문서 Worker는 한 Batch 응답을 받은 뒤 다음 요청을 보내는 순차 구조다. 따라서 전체 문서 처리량을
높이면서 단일 요청 지연을 2초 안팎으로 유지하는 Batch 32가 기존 16보다 낫다.

### 5.2 Batch 32에서 64

- 처리량 `17.08 → 17.38 texts/s`: `1.76%` 증가
- Batch 32는 최고 처리량의 `98.27%`
- p95 요청 지연 `1.98 → 3.72초`: `1.88배` 증가
- 최고 RSS `2,176.68 → 2,259.72MiB`: 약 `83.05MiB` 증가

Batch 64는 처리량 최고값이지만 추가 이득이 작고 꼬리 지연과 메모리 증가가 크다. 기본값으로
선택하지 않고 더 큰 CPU·GPU 환경에서 재측정할 수 있는 상한으로 유지한다.

## 6. 기본값 판정

| 기준 | Batch 32 결과 | 판정 |
|---|---:|---|
| 실패율 0 | 0% | 통과 |
| 최고 처리량의 95% 이상 | 98.27% | 통과 |
| Batch 64 대비 p95 절감 | 약 46.89% | 통과 |
| Batch 64 대비 최고 RSS 절감 | 약 83.05MiB | 통과 |

최종 판정은 다음과 같다.

```text
EMBEDDING_DOCUMENT_BATCH_SIZE 기본값: 16 → 32
허용 범위: 1~64 유지
Query Embedding 단건 API: 변경 없음
```

## 7. 자동 검증

### 7.1 Python 계약 테스트

```bash
python3 -m pytest -p no:cacheprovider \
embedding-server/test_benchmark_batch_size.py \
embedding-server/test_main.py
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

일회성 Embedding Container에서 `28 passed`를 확인했다.

검증 범위:

- 결정적 Corpus와 Batch Size 설정 검증
- Percentile 선형 보간
- Round별 Profile 순서 회전
- Model·Index·개수·차원·유한값 검증
- 처리량·p50·p95·p99·RSS 집계
- 현재 기본값 유지·변경 신호 계산
- 기존 `/embed`, `/embed/batch` 계약 회귀

### 7.2 실제 모델 Smoke

64개 Text·1회 Smoke에서도 Batch Size 6종, 95개 요청이 모두 성공했다. 이 결과는 실행기와 RSS
수집 경계 확인에만 사용하고 기본값 판단은 256개 Text·3회 확장 결과로 수행했다.

### 7.3 Java 전체 회귀

첫 실행은 현재 PostgreSQL Container가 Host `5432`에 노출된 것과 Test 기본 포트 `55432`가 달라
DB 기반 Test Context가 초기화되지 않았다. 저장소 설정을 변경하지 않고 Test Process에 실제 로컬
포트와 SSL Mode, 일회성 JWT를 주입해 다시 실행했다.

```bash
DB_PORT=5432 \
DB_SSLMODE=disable \
JWT_SECRET=<test-only-value> \
./gradlew test
```

결과:

```text
tests=709 failures=0 errors=0 skipped=0
```

## 8. 한계와 후속 작업

- CPU 단일 요청 Benchmark이므로 여러 Worker의 동시 요청 처리량은 포함하지 않는다.
- RSS는 1초 간격과 Profile 경계의 Container PID 1 표본이며 순간 Peak를 놓칠 수 있다.
- 입력은 결정적 한국어 문장 집합이지만 실제 운영 문서의 Token 길이 분포 전체를 대표하지 않는다.
- GPU 환경은 처리량·메모리 곡선이 다르므로 같은 명령으로 다시 측정해야 한다.
- Worker 수평 확장과 Queue Backpressure는 별도 성능 작업에서 검증한다.
Loading