feat(embed): candle 임베딩 provider (NUMA-안전, opt-in) #199
Reference in New Issue
Block a user
Delete Branch "feat/embed-candle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
CPU-only 듀얼소켓 NUMA 서버(Xeon Silver 4214 ×2 = 48 logical)에서
kebab ingest가 매 실행double free or corruption (!prev)로 죽는 문제를 해결한다. 근본 원인은 fastembed 4.9.1 이 onnxruntime intra-op 스레드를available_parallelism()(=48) 로 하드코딩하고InitOptions에 override API 가 없어, NUMA 에서 onnxruntime 스레드풀이 힙을 손상시키는 것이다 (코드로 확정 — fastembedtext_embedding/impl.rs:52,80).해법으로 동일 모델
intfloat/multilingual-e5-large를 candle(순수 Rust) 로 돌리는 임베딩 provider 를 추가한다. 순수 Rust 라 네이티브 힙 손상이 구조적으로 불가능하고, candle 의 CPU 백엔드는 rayon 글로벌 풀 크기로 스레드를 정하므로 NUMA-안전한 수로 캡할 수 있다. opt-in (models.embedding.provider = "candle") — 글로벌 default 는 fastembed 유지(일반 머신에선 더 빠름).설계: docs/superpowers/specs/2026-06-01-embed-candle-track-spec.md (우산: 같은 디렉터리
2026-06-01-embedding-numa-backends-meta-spec.md/-meta-plan.md)변경
crates/kebab-embed-candle—kebab_core::Embedder구현 (CandleEmbedder). safetensors via hf-hub →XLMRobertaModelforward(CPU) → attention-mask 가중 mean pooling → L2 → e5query:/passage:prefix. candle 의존성 트리를 이 crate 에 격리 (kebab-core/kebab-config 외 다른 kebab-* 의존 0 — design §8 경계).[models.embedding].num_threads(u32, default 0=auto) + envKEBAB_EMBED_THREADS(우선).CandleEmbedder::new에서 글로벌 rayon 풀 1회 캡.kebab-app::App::embedder()가provider분기 —fastembed/onnx/(빈값) → 기존FastembedEmbedder(동작·벡터 불변),candle→CandleEmbedder, 미지값 → 에러. 비-e5-large 모델은 다운로드 전 fail-fast(모델 가드).검증
cargo clippy --workspace --all-targets -j 4 -- -D warnings→ exit 0, warning 0.cargo test -p kebab-embed-candle -p kebab-config -j 4→ exit 0 (candle 단위 6 + thread_cap 1 + parity 1 ignored, config 68). 독립 재실행으로 재확인.FastembedEmbedder, 동일 10문장(한/영, document+query 양쪽 prefix) → cosine_min = 1.000000, 차원별 max 절대오차 = 2.01e-7 (f32 커널 반올림 수준).embedding_version유지 → 기존 LanceDB 색인 재사용. 재현:crates/kebab-embed-candle/tests/parity.rs(--ignored, 모델 ~2GB 필요).num_threadsserde default(0)로 파싱, wire schema 변경 없음.시험 항목 (Test Plan)
provider=candle로 5150-doc ingest double-free 없이 EXIT=0 완주 — meta-spec §4.3, 본 PR 의 최종 인수 게이트비범위 / 후속
Assisted-by: Claude Code
회차 1 — 독립 재리뷰(opus). Critical/High/Medium 0건. 임베딩 파이프라인(prefix·mask mean pool·L2), provider 분기 무회귀, 모델 가드(다운로드 전 fail-fast + 단위테스트), §8 의존 경계, 패리티 query/passage 양쪽 커버, 문서/HOTFIXES 정합 모두 확인. clippy 0. SMOKE.md candle 예시 nit 1건만 반영 요청.
@@ -108,3 +108,3 @@[models.embedding]provider = "fastembed" # "none" 으로 두면 lexical-only — Ollama 불필요provider = "fastembed" # "fastembed"(기본) / "candle"(순수 Rust, NUMA-안전)nit: 이 예시는
model = multilingual-e5-small/dimensions = 384인데 주석에candle을 제시한다. 사용자가 이 파일에서provider = "candle"로만 바꾸면 candle 모델 가드(SUPPORTED_MODEL = multilingual-e5-large)에 걸려 에러가 난다. candle 사용 시model = multilingual-e5-large/dimensions = 1024도 함께 바꿔야 함을 한 줄로 명시하면 self-correcting 에러에 의존하지 않아도 된다.회차 2 — 회차 1 nit 반영 확인(SMOKE.md 에 provider=candle 시 model/dimensions 동반 변경 주의 명시,
edac3ae). 잔여 actionable 0건. 코드 정확·clippy 0·테스트 녹색·문서 정합·패리티 2.01e-7 로 재색인 0 근거 충족. 머지 동의. 유일 잔여 게이트는 사용자 실행 NUMA 서버 5150-doc 완주(meta-spec §4.3) — 코드 레벨 승인과 무관.회차 3 — 증거 커밋(
d85d734) 검토. 도그푸딩(997 docs/23,151 chunks/에러 0), A1(taskset -c 0-3) 실서버 반증, MKL 부정 결과(38~50% 느림)를 HOTFIXES + release-notes 두 곳에 정확히 기록. 수치는 실측과 일치, 문서만 변경(코드/테스트 무영향). 잔여 actionable 0건. candle 트랙 코드 레벨 완료 — 머지 동의. 유일 잔여는 사용자 NUMA 서버 실배포 검증(코드 승인과 무관).