Files
kebab/docs/superpowers/specs/2026-06-01-embed-candle-track-spec.md
altair823 6ec4e6809f fix(embed-candle): address round-1 review
- commit track-spec + meta-spec/plan into branch (HIGH: dangling `amends:` ref)
- inline parity evidence (cosine 1.0, max_abs_diff 2.01e-7) into HOTFIXES +
  release notes; drop refs to deleted IMPL_REPORT/SPIKE_REPORT (MEDIUM)
- model guard: reject non-e5-large `model` before the 2GB download so
  model_id() can't mislabel vectors (MEDIUM) + unit test
- parity test now covers BOTH query: and passage: prefixes (MEDIUM)
- guard encodings.first() index; document zero-attention/pooling invariant;
  clarify embed_batch prefixing doc (LOW)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:54:20 +00:00

79 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Track 1 Spec — candle e5-large 임베딩 provider (NUMA-안전)
- 날짜: 2026-06-01
- 우산: [meta-spec](./2026-06-01-embedding-numa-backends-meta-spec.md) / [meta-plan](./2026-06-01-embedding-numa-backends-meta-plan.md)
- 선행: Phase 0 스파이크 PASS+독립검증 (cosine 1.000000, 스레드 캡 가능, latency ~4×). 커밋 76841af.
- 브랜치: `feat/embed-candle`
## 1. 목표
fastembed(onnxruntime) 의 "intra-op 스레드 48 하드코딩 → NUMA 힙 손상" 을 회피하기 위해, 동일 모델 `multilingual-e5-large`**candle(순수 Rust)** 로 돌리는 임베딩 provider 를 추가한다. opt-in, 품질 중립, NUMA 스레드 캡 가능.
## 2. 확정 결정 (사용자 승인 2026-06-01)
- **D-reindex**: `embedding_version` **유지(재색인 0)** 를 목표. 구현 중 candle vs onnxruntime 벡터의 **차원별 max 절대오차**를 측정해 사실상 동일(예: max abs diff < 1e-5)함을 확인하고, 골든 스위트로 회귀 0 을 실측해 확정. 유의미한 차이가 나오면만 version bump + 재색인.
- **D-default**: 글로벌 default provider 는 **onnxruntime 유지**, candle 은 **opt-in** (`models.embedding.provider = "candle"`).
- **조기 종료**: candle 이 골든 baseline 충족 시 ollama/A2 트랙 생략 (A1 stopgap 문서만 별도).
## 3. 아키텍처
- **신규 crate `kebab-embed-candle`** — `kebab_core::Embedder` 구현. candle 의 큰 의존성 트리를 이 crate 에 격리.
- 허용 deps: `candle-core`/`candle-nn`/`candle-transformers` (0.10.x), `tokenizers`, `hf-hub`, `kebab-core`, `kebab-config`, `anyhow`, `tracing`. **다른 `kebab-*` 의존 금지**(core/config 외) — design §8 경계.
- **주입 분기**: `kebab-app/src/app.rs``embedder()` (현 :829-837, `FastembedEmbedder::new` 무조건 생성) 를 `config.models.embedding.provider` 로 분기:
- `"fastembed"` | `"onnx"` | (빈값/기존) → `FastembedEmbedder` (default, 기존 동작 유지).
- `"candle"``CandleEmbedder`.
- 알 수 없는 값 → 명확한 에러.
- **facade 규칙 준수**: UI crate 는 `kebab-app` 만. `kebab-app``kebab-embed-candle` 의존 추가.
## 4. CandleEmbedder 동작 (스파이크에서 검증된 파이프라인)
- 모델: `intfloat/multilingual-e5-large``model.safetensors` + `config.json` + `tokenizer.json``hf-hub` 으로 `{model_dir}/candle/` (config `storage.model_dir`) 아래에 캐시.
- `candle_transformers::models::xlm_roberta::{Config, XLMRobertaModel}` 로 로드 (CPU `Device::Cpu`).
- `embed()`: e5 프리픽스(`query: `/`passage: `, `EmbeddingInput` kind 기준 — `kebab-embed-local``prefix_input` 규약과 동일) → 토크나이즈(max_len 512, batch-longest 패딩, special tokens) → forward → **attention-mask 가중 mean pooling****L2 정규화**.
- `dimensions()` = 1024, `model_id`/`model_version` = config 값(기존과 동일 식별자 유지).
- **스레드 캡**: config 신규 필드 `models.embedding.num_threads`(u32, 0=auto) + env `KEBAB_EMBED_THREADS`. `CandleEmbedder::new` 에서 `rayon::ThreadPoolBuilder::new().num_threads(n).build_global()` 1회 적용(이미 초기화 시 무시). 0/auto 면 미설정(rayon 기본). NUMA 노드 바인딩은 `numactl`(A1) 과 조합 — 문서화.
- `Mutex<XLMRobertaModel>` 또는 forward 가 `&self` 면 불필요 — candle forward 는 `&self` 가능, 단 내부 가변 없으면 `Send+Sync` 보장 확인.
## 5. config 변경
- `EmbeddingModelCfg``num_threads: u32`(default 0) 추가. env `KEBAB_EMBED_THREADS`.
- `provider` 허용값 문서화: `fastembed`(default)/`candle`.
- default toml + `Config::default()` 갱신, 기존 테스트 영향 확인.
## 6. 버전/캐스케이드
- D-reindex 에 따라 `embedding_version` 유지 (벡터 동일). cascade(design §9) 트리거 안 함 — 기존 색인 재사용. (max abs diff 확인 실패 시에만 bump.)
- wire schema 변경 없음.
## 7. 테스트 (산출물)
- **단위**(`kebab-embed-candle`): `dimensions()==1024`; `embed()` 출력 L2≈1; 빈 입력 빈 출력; 프리픽스 적용 확인.
- **패리티 테스트**(`#[ignore]`, 모델 2GB+네트워크 필요): candle vs `FastembedEmbedder` 동일 문장 cosine ≥ 0.9999 + max abs diff 보고. CI 기본 제외, 수동/도그푸딩에서 실행.
- **통합**(`kebab-cli` 또는 `kebab-app`): `provider="candle"` 로 소량 fixture ingest → 청크/임베딩 카운트 > 0, 검색 1건 성공. (모델 필요 → `#[ignore]` 또는 feature.)
- **스레드 캡**: `num_threads=4` 설정 시 `rayon::current_num_threads()==4` 확인.
- **회귀**: 기존 fastembed 경로 default 동작 불변(provider 미지정 시).
- clippy `-D warnings`, 빌드 직렬 `-j 4`.
## 8. 품질 게이트 (머지 전)
- `kebab-eval` 골든 스위트(`/build/dogfood/golden_queries.yaml`) 를 provider=candle 로 실행 → MRR/hit@k ≥ 현 baseline (회귀 0). [[feedback_search_quality_dogfood]]
- 패러프레이즈 robustness(#195/#196) 스폿 확인.
## 9. 문서/릴리스 (머지 시 동일 PR)
- README: Configuration 에 `provider=candle` + `num_threads`/`KEBAB_EMBED_THREADS` 추가. SMOKE config 예시 동기화. [[feedback_readme_sync_rule]]
- ARCHITECTURE: crate 그래프 + 디렉터리에 `kebab-embed-candle` 추가.
- HANDOFF: 머지 후 한 줄(임베딩 백엔드 다변화).
- HOTFIXES: 본 날짜 dated entry (NUMA double-free 진단 + candle provider 도입 + 스파이크 패리티 증거).
- 버전 bump: 신규 config surface(provider=candle, num_threads) = pre-1.0 minor bump (0.21.1 → 0.22.0), release notes.
## 10. 범위 밖 / 후속
- candle crate feature-gate 로 빌드 비용 격리 (후속).
- NUMA 노드 자동 바인딩(현재는 numactl 외부 조합).
- ollama/A2/A1 트랙 (candle 게이트 통과 시 생략).
## 11. 잔여 게이트 (사용자 실행, Claude 불가)
- 그 듀얼소켓 NUMA 서버에서 `provider=candle` 로 5150-doc ingest **double-free 없이 EXIT=0 완주**. PR 머지 전/후 검증 예약. (meta-spec §4.3)