feat(embed): candle 임베딩 provider (NUMA-안전, opt-in) + v0.22.0
duo-socket NUMA 서버에서 fastembed(onnxruntime)가 intra-op 스레드를 48개로 하드코딩해 NUMA 힙 손상 → double-free 로 ingest 가 죽는 문제를 회피하기 위해, 같은 multilingual-e5-large 모델을 순수 Rust(candle)로 돌리는 opt-in 임베딩 provider 를 추가한다. - 신규 crate kebab-embed-candle: CandleEmbedder (kebab_core::Embedder). hf-hub safetensors → XLMRobertaModel forward → mask mean-pool → L2 → e5 prefix. candle 의존성 트리를 이 crate 에 격리 (core/config 외 kebab-* 의존 0). - 스레드 캡: [models.embedding].num_threads + env KEBAB_EMBED_THREADS → 글로벌 rayon 풀 1회 캡 (NUMA-안전 레버). - kebab-app::embedder() 가 provider 분기 (fastembed/onnx/"" → 기존 경로 불변, candle → CandleEmbedder, 미지값 → 에러). - Phase 0 스파이크 crate 제거 (production 흡수). - 버전 0.21.1 → 0.22.0 (신규 config surface, pre-1.0 minor bump). 패리티: cosine_min=1.000000, max abs diff=2.01e-7 (< 1e-5) → embedding_version 유지, 재색인 0. fastembed default 동작/벡터 불변. wire schema 변경 없음. 검증(파일+exit code): clippy -D warnings EXIT=0(warning 0), test EXIT=0 (candle unit 5 + thread_cap rayon=4 + config 68), parity #[ignore] EXIT=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,8 @@ flowchart TB
|
||||
end
|
||||
subgraph Adapters ["traits + adapters"]
|
||||
embed["kebab-embed<br/>(trait)"]
|
||||
embedlocal["kebab-embed-local<br/>(fastembed)"]
|
||||
embedlocal["kebab-embed-local<br/>(fastembed, default)"]
|
||||
embedcandle["kebab-embed-candle<br/>(candle, NUMA-safe opt-in)"]
|
||||
llm["kebab-llm<br/>(trait)"]
|
||||
llmlocal["kebab-llm-local<br/>(Ollama)"]
|
||||
search["kebab-search"]
|
||||
@@ -92,6 +93,7 @@ flowchart TB
|
||||
app --> sqlite
|
||||
app --> vector
|
||||
app --> embedlocal
|
||||
app --> embedcandle
|
||||
app --> llmlocal
|
||||
app --> search
|
||||
app --> rag
|
||||
@@ -104,6 +106,8 @@ flowchart TB
|
||||
paud --> core
|
||||
pcode --> core
|
||||
embedlocal --> embed
|
||||
embedcandle --> core
|
||||
embedcandle --> config
|
||||
llmlocal --> llm
|
||||
rag --> search
|
||||
rag --> llm
|
||||
@@ -180,6 +184,7 @@ kebab/
|
||||
│ ├── kebab-store-sqlite/ # SQLite + FTS5 (V001/V002/V003) (P1-6, P2-1, P3-3). src/derivation_cache.rs = derivation_cache 테이블 저장소 (V012, v0.21.0)
|
||||
│ ├── kebab-search/ # Lexical + Vector + Hybrid retriever (P2-2, P3-4)
|
||||
│ ├── kebab-embed/ kebab-embed-local/ # Embedder trait + fastembed adapter (P3-1, P3-2)
|
||||
│ ├── kebab-embed-candle/ # candle (pure-Rust) Embedder, NUMA-safe opt-in provider=candle (Track 1, v0.22.0)
|
||||
│ ├── kebab-store-vector/ # LanceDB VectorStore (P3-3, P7-3 follow-up)
|
||||
│ ├── kebab-llm/ kebab-llm-local/ # LanguageModel trait + Ollama adapter (P4-1, P4-2)
|
||||
│ ├── kebab-rag/ # RAG pipeline (P4-3)
|
||||
|
||||
@@ -107,11 +107,13 @@ respect_markdown_headings = true
|
||||
chunker_version = "md-heading-v1"
|
||||
|
||||
[models.embedding]
|
||||
provider = "fastembed" # "none" 으로 두면 lexical-only — Ollama 불필요
|
||||
provider = "fastembed" # "fastembed"(기본) / "candle"(순수 Rust, NUMA-안전)
|
||||
# / "none"(lexical-only — Ollama 불필요)
|
||||
model = "multilingual-e5-small"
|
||||
version = "v1"
|
||||
dimensions = 384
|
||||
batch_size = 64
|
||||
num_threads = 0 # candle 전용 CPU 스레드 캡 (0=auto). env KEBAB_EMBED_THREADS 우선.
|
||||
|
||||
[models.llm]
|
||||
provider = "ollama"
|
||||
|
||||
72
docs/release-notes/v0.22.0-draft.md
Normal file
72
docs/release-notes/v0.22.0-draft.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: kebab v0.22.0 release notes (draft)
|
||||
created: 2026-06-01
|
||||
status: draft
|
||||
release_trigger:
|
||||
- 신규 config surface (provider=candle, num_threads / KEBAB_EMBED_THREADS) — pre-1.0 minor bump
|
||||
- 임베딩 백엔드 다변화 (NUMA-안전 candle provider 추가, opt-in)
|
||||
---
|
||||
|
||||
# kebab v0.22.0 — candle 임베딩 provider (NUMA-안전, opt-in)
|
||||
|
||||
v0.21.1 (config 마이그레이션) 후속 minor release. 듀얼소켓 NUMA 서버에서
|
||||
onnxruntime 의 스레드 하드코딩이 일으키던 ingest 크래시를 피하기 위해, 같은
|
||||
임베딩 모델을 **순수 Rust(candle)** 로 돌리는 opt-in provider 를 추가한다.
|
||||
**기본 동작은 그대로다** — 기존 사용자는 아무것도 바꿀 필요가 없다.
|
||||
|
||||
---
|
||||
|
||||
## 핵심 변경
|
||||
|
||||
### candle 임베딩 provider (`provider = "candle"`)
|
||||
|
||||
**변경 사실.** `[models.embedding].provider` 에 `"candle"` 값이 추가됐다.
|
||||
`"fastembed"`(기본, onnxruntime) / `"candle"`(순수 Rust) / `"none"`(lexical-only)
|
||||
중 하나를 고를 수 있다. candle provider 는 fastembed 와 **완전히 같은 모델**
|
||||
(`intfloat/multilingual-e5-large`, 1024-dim)을 쓰고, e5 prefix → mean pooling
|
||||
→ L2 정규화 파이프라인도 동일하다. 첫 사용 시 safetensors(~2GB)를
|
||||
`{model_dir}/candle/` 아래로 자동 다운로드한다.
|
||||
|
||||
```toml
|
||||
[models.embedding]
|
||||
provider = "candle" # 기본은 "fastembed" — NUMA 서버에서만 candle 권장
|
||||
num_threads = 8 # candle CPU 스레드 캡 (0 = auto = #cores)
|
||||
```
|
||||
|
||||
```bash
|
||||
# env 로도 캡 가능 (config 보다 우선)
|
||||
KEBAB_EMBED_THREADS=8 kebab ingest
|
||||
```
|
||||
|
||||
**Trade-off.** candle 는 순수 Rust 라 onnxruntime 의 네이티브 SIMD 커널보다
|
||||
CPU latency 가 느리다 (Phase 0 스파이크 측정 ~4×). 그래서 **기본값은
|
||||
fastembed 를 유지**하고, candle 은 onnxruntime 가 죽는 NUMA 환경에서만 켜는
|
||||
opt-in 으로 둔다. 단일 워크스테이션 사용자는 fastembed 가 더 빠르다.
|
||||
|
||||
**Mitigation (왜 안전한가).** candle 의 CPU 백엔드는 글로벌 rayon 풀 크기로
|
||||
스레드를 정한다. `num_threads`(또는 env `KEBAB_EMBED_THREADS`)가 그 풀을 한 번
|
||||
캡하므로, onnxruntime 가 하드코딩하던 48 intra-op 스레드 → NUMA 힙 손상 →
|
||||
double-free 경로를 원천 차단한다. NUMA 노드 바인딩이 더 필요하면 `numactl`
|
||||
과 조합한다.
|
||||
|
||||
**Upgrade 절차.** 재색인 **불필요**. candle 과 fastembed 의 벡터는 사실상
|
||||
동일(Phase 0 스파이크 코사인 1.000000)해서 `embedding_version` 을 유지했고,
|
||||
기존 LanceDB 색인을 그대로 재사용한다. provider 를 바꿔도 검색 결과는
|
||||
바뀌지 않는다. 기존 `config.toml` 은 `num_threads` 가 자동으로 `0`(auto)으로
|
||||
채워져 그대로 로드된다 — `kebab config migrate` 도, 수동 편집도 필요 없다.
|
||||
|
||||
---
|
||||
|
||||
## 그 외
|
||||
|
||||
- 신규 crate `kebab-embed-candle` (candle 의존성 트리를 이 crate 에 격리,
|
||||
`kebab-core`/`kebab-config` 외 다른 kebab-* 의존 없음).
|
||||
- Phase 0 feasibility 스파이크(`spike-embed-candle`)는 production 흡수 후 제거.
|
||||
- 문서: README Configuration, `docs/SMOKE.md` config 예시, `docs/ARCHITECTURE.md`
|
||||
crate 그래프/트리에 candle provider 반영.
|
||||
|
||||
## 잔여 검증 (사용자 실행)
|
||||
|
||||
듀얼소켓 NUMA 서버에서 `provider=candle` 로 5150-doc ingest 가 double-free
|
||||
없이 EXIT=0 완주하는지가 본 release 의 최종 인수 게이트다 (meta-spec §4.3).
|
||||
패리티 max abs diff 수치는 `IMPL_REPORT.md` 참조.
|
||||
Reference in New Issue
Block a user