# M3 — bge-m3 batch 임베딩 throughput Bench

**측정일**: 2026-05-02
**대상**: GPU server Ollama `:11434` `bge-m3` (latest, 1.2GB, 1024d)
**범위**: read-only `/api/embed` 호출만. 4.0 가드 준수 (compose / migration / queue / worker restart / source_channel insert / SearXNG 도입 모두 0건). Ollama 모델/컨테이너 변경 없음.
**bench 스크립트**: `/tmp/bench_M3_embed.py` (격리, GPU 서버 내 실행)
**plan 참조**: `~/.claude/plans/users-hyungiahn-library-cloudstorage-sy-binary-dongarra.md` §4.

---

## 1. 사전 점검

| 항목 | 상태 |
|---|---|
| Ollama `/api/tags` healthcheck | OK (`gemma4:e4b-it-q8_0`, `bge-m3:latest` 2개) |
| 진행 중 embed worker (사전) | embed pending 17건, processing 0건 — **운영 worker idle 직전** |
| 측정 방식 | 단발 호출 3회씩, batch 1/8/32 — 운영 영향 최소 |

embed worker 와 시간대 분리 — 측정 호출이 ~1초 미만이라 worker idle 시간에 끼어들지 않음.

---

## 2. 시나리오

| 항목 | 값 |
|---|---|
| 입력 텍스트 | ~250 토큰 한·영 mixed (ASME B16.5 / KGS Code / 가스기사 sample), chunk #N 으로 변형해서 unique 32 inputs 생성 |
| API | Ollama `/api/embed`, body `{model, input: [...]}` |
| measure | 각 batch_size 당 3회 반복 |
| batch_size | 1, 8, 32 (plan 기본 범위) — 128 미실시 |

응답: 각 batch 마다 `embeddings: [[1024d], ...]`, `dim=1024` 일관.

---

## 3. 측정 결과

### batch=1 (3회)
```
repeat 1: ok elapsed=0.10s embeddings=1 dim=1024 throughput=10.7 chunks/s
repeat 2: ok elapsed=0.08s embeddings=1 dim=1024 throughput=12.7 chunks/s
repeat 3: ok elapsed=0.08s embeddings=1 dim=1024 throughput=12.6 chunks/s
```

### batch=8 (3회)
```
repeat 1: ok elapsed=0.12s embeddings=8 dim=1024 throughput=67.9 chunks/s
repeat 2: ok elapsed=0.12s embeddings=8 dim=1024 throughput=65.2 chunks/s
repeat 3: ok elapsed=0.12s embeddings=8 dim=1024 throughput=68.6 chunks/s
```

### batch=32 (3회)
```
repeat 1: ok elapsed=0.26s embeddings=32 dim=1024 throughput=122.6 chunks/s
repeat 2: ok elapsed=0.25s embeddings=32 dim=1024 throughput=126.4 chunks/s
repeat 3: ok elapsed=0.25s embeddings=32 dim=1024 throughput=130.5 chunks/s
```

| batch | n | avg (s) | min (s) | max (s) | throughput (chunks/s) | error |
|---:|---:|---:|---:|---:|---:|---:|
| 1 | 3 | 0.088 | 0.079 | 0.106 | 11.4 | 0 |
| 8 | 3 | 0.119 | 0.117 | 0.123 | 67.2 | 0 |
| 32 | 3 | **0.253** | 0.245 | 0.261 | **126.4** | 0 |

**관찰**:
- 모든 batch 0 error.
- batch 32 가 batch 1 대비 throughput 11배 — GPU batch 가속 큰 효과.
- batch 8 → 32 는 1.88×만 더 빨라짐 → batch 32 부근에서 throughput 증가 곡선 완만 (batch 128 측정 굳이 필요 없음을 시사).

---

## 4. plan 4.1 게이트 판정 (M3 관련)

### 게이트 4 — 150 chunks/일 임베딩 ≤ 1분 (batch 32)

| 항목 | 값 |
|---|---|
| 150 chunks 처리에 필요한 batch 수 | ⌈150 / 32⌉ = **5 batches** |
| 1 batch 평균 시간 | 0.253s |
| 150 chunks 총 시간 | 5 × 0.253s = **1.27s** |
| 임계값 | ≤ 60s |
| **판정** | **✓ PASS** (여유 47×) |

압도적 여유. embed 자원은 LocalResearch 도입의 병목이 아님.

---

## 5. 게이트 5 — 1C/1D 충돌 점검 (read-only schema 분석)

```sql
-- documents.source_channel 분포
news 9024, manual 638, law_monitor 206, drive_sync 95, inbox_route 6, memo 6, NULL 1
-- documents.content_origin 분포
extracted 9976
-- process_stage enum values
extract, classify, embed, preview, summarize, chunk, stt, thumbnail, deep_summary, markdown
```

| 점검 | 현황 | LocalResearch 영향 | 판정 |
|---|---|---|---|
| `documents.source_channel='web'` 값 | 미사용 (현재 6개 값 + NULL 1건) | 신규 값 추가 필요. CHECK 제약 없음. 충돌 0 | ✓ |
| `documents.content_origin='imported'` 값 | **0건** (extracted 9976만) | Phase 1A 에서 정의된 'imported' 슬롯이 비어있어 LocalResearch 가 자연스럽게 활용 가능 | ✓ |
| `documents.md_content` (Phase 1A) | nullable, optional-use | LocalResearch가 web→markdown 변환 후 동일 컬럼 활용 → Phase 1C `<MarkdownDoc>` 렌더러 자동 재사용 | ✓ (오히려 시너지) |
| `document_lineage` 테이블 (Phase 1A) | 빈 테이블, RESTRICT FK | LocalResearch web 문서의 출처 추적 (search_query → urls)에 자연 활용 | ✓ (오히려 시너지) |
| `process_stage` enum | 10개 stage. fetch/web_extract 부재 | LocalResearch Phase 2 (수집기) 진입 시 ADD VALUE migration 필요. **본 feasibility 단계에선 변경 0**. Phase 1C/1D 와는 stage 충돌 0 (markdown stage 공존 가능) | ✓ |
| Phase 1C `<MarkdownDoc>` UI | 미배포 (다음 차례) | LocalResearch 와 같은 컴포넌트 재사용 — 충돌 0, 시너지 | ✓ |
| Phase 1D pilot (30건 quality 측정) | 미시작 | LocalResearch 와 측정 시간대 분리만 하면 무관 | ✓ |

**판정**: **✓ PASS**. 인접 트랙과의 schema/queue/UI 충돌 0. Phase 1A markdown canonical layer 가 오히려 LocalResearch 진입 기반 인프라 역할.

---

## 6. LocalResearch Phase 1 진입 게이트 종합 판정

| # | 게이트 | 측정값 | 임계값 | 판정 |
|---|---|---|---|---|
| 1 | queue capacity 여유 (M1) | classify peak +1.5%p, 전체 LLM stage +2.5%p (peak day) | ≤ 10% 추가 부하 | ✓ PASS |
| 2 | Gemma 30건/일 (M2) | 30 × 3.51s = 1.76분 (concurrency 1) | ≤ 10분 | ✓ PASS |
| 3 | concurrency 2 마진 (M2) | error_rate 0% (단 latency 9.5×) | error_rate = 0 | ✓ PASS |
| 4 | 임베딩 150 chunks/일 (M3) | 5 × 0.253s = 1.27s (batch 32) | ≤ 1분 (60s) | ✓ PASS |
| 5 | 1C/1D 충돌 (M3) | source_channel/content_origin/md_content/lineage 모두 비충돌 | 충돌 없음 | ✓ PASS |

### **종합: PASS (5/5)**

추가 관찰:
- **M2 N=2 latency 9.5× 증가** 는 본질적 MLX concurrency=1 특성으로 새로운 위험 아니지만, **Phase 1 본격 plan 단계에서 web 요약 워커는 background queue + concurrency 1 직렬 (실시간 아님)** 로 설계 권장. ask 트래픽과 동일 시간대 경합 시 양쪽 모두 5~10× 지연 발생.
- **content_origin='imported' 슬롯이 0건으로 비어있는 점**이 인상적 — Phase 1A 가 LocalResearch 진입을 의식하지 않고 설계됐는데도 자연스러운 자리 확보. document_lineage 테이블도 동일.
- **process_stage 'fetch' / 'web_extract' / 'summarize_web' 추가**는 LocalResearch Phase 2 본격 plan 단계 작업. 기존 enum 10개와 충돌 없음. ORM enum 동시 갱신 필요 (메모리 `feedback_orm_db_enum_sync.md`).

---

## 7. STOP — 사용자 검토 대기

plan §4.2 명시된 **Stop Point** 도달:

> M3 보고서 + 게이트 판정 표 작성 직후 작업 정지.
> LocalResearch Phase 1 상세 plan 작성, SearXNG 도입, 코드 변경, plan 모드 재진입 등 일체 금지.
> 사용자가 게이트 판정 검토 후 다음 액션을 명시적으로 지시할 때까지 대기.

본 reports 3개 (`local_research_M1_throughput.md` + `_M2_mlx_capacity.md` + `_M3_embedding_throughput.md`) 가 dashboard 산출물. 코드/스키마 변경 0, inventory soft lock 위반 0, queue stage 변경 0.

다음 액션은 사용자 결정.
