Skip to content

feat(#162): hnsw_export halfvec 지원 — dim 상한 문제 해결 - #185

Merged
ysys143 merged 8 commits into
mainfrom
feat/162-halfvec-hnsw-export
Aug 7, 2026
Merged

feat(#162): hnsw_export halfvec 지원 — dim 상한 문제 해결#185
ysys143 merged 8 commits into
mainfrom
feat/162-halfvec-hnsw-export

Conversation

@ysys143

@ysys143 ysys143 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Closes feat: hnsw_import halfvec 지원 — dim=3072 경로 부재 + 1024/1536 인덱스 2~3배 #162
  • CAGRA GPU 빌드는 fp32 그대로 두고(cuVS는 fp16 CAGRA 미지원, ADR-054), write_elem_page()의 마지막 memcpy에서만 halfvec으로 narrow-cast — float_to_half()는 손수 구현한 IEEE-754 round-to-nearest-even이며 라이브 pgvector 0.8.6과 335/335 비트 단위 일치 검증(round-to-even tie 케이스 포함).
  • halfvec_{l2,ip,cosine}_ops 3종 opclass를 pg_cuvs_hnsw AM에 등록(0.8.0, SQL-visible 신규 표면이라 마이너 범프).
  • dim 상한 체크(check_hnsw_page_fit)를 GPU 작업 전으로 당겨 고정 safe_maxlevel=5로 조기 거부 — 기존엔 CAGRA 빌드+hnswlib 파일 생성이 끝난 뒤에야 거부돼 GPU 시간을 낭비했다.
  • 이제 halfvec 경유로 dim=3072(vector HNSW 상한 2000으로는 도달 불가)까지 인덱싱 가능.

Commits (Tidy First: 구조 → 동작 → 동작 → 테스트)

  1. refactor: elem_tuple_size/write_elem_pagebytes_per_dim으로 파라미터화 (동작 불변)
  2. feat: halfvec 쓰기 경로 + 카탈로그 타입 판별 + opclass SQL + 버전 핀 (0.8.0)
  3. feat: dim 상한 체크를 GPU 작업 전으로 이동
  4. test: halfvec 커버리지 — 신규 build_hnsw_halfvec.sql, dim 상한 거부 케이스, AM 경로 halfvec 케이스

Test plan

  • make gpu-deploymake gpu-verify-deployed (6항목 전부 GREEN)
  • make installcheck 40/40 GREEN, 2회 연속 재현으로 안정성 확인(신규 테스트에 OID 플레이키니스 없음)
  • make installcheck-isolation 6/6 GREEN
  • float_to_half() 335/335 라이브 pgvector 0.8.6 대비 비트 단위 일치 (round-to-even tie 케이스 포함)
  • 수기 스모크: dim=8/18벡터, mode=nsw·hnswlib 양쪽 — 검색 정합성 + 재빌드 결정성
  • dim=1900(fp32,M=32)·dim=3600(halfvec,M=32) 사전 거부가 GPU/IPC 왕복 전에 발생함을 daemon 로그로 확인
  • recall 측정(fp32 vs halfvec, dim=1536/3072, wiki_all_1M self-concatenation 재사용) — 별도 후속 커밋으로 진행 예정

ysys143 added 6 commits August 7, 2026 02:09
동작 변경 없음(Tidy First 구조적 커밋) — 모든 호출부가 리터럴 4를 전달하므로
지금은 항상 fp32와 동일하게 동작한다. halfvec 배선은 다음 커밋에서.

- elem_tuple_size(dim) -> elem_tuple_size(dim, bytes_per_dim)
- write_elem_page()의 esize/vl_len 계산에 bytes_per_dim 추가. memcpy 자체는
  아직 무조건 fp32(dim*sizeof(float)) — halfvec 분기는 다음 커밋.
- 중복된 두 페이지-fit 체크(:1064-1077 근방, :1636-1648 근방)를 공유 헬퍼
  check_hnsw_page_fit(dim, bytes_per_dim, m, maxlevel)로 추출. 에러 문구를
  하나로 통일(bytes_per_dim 필드 추가) — 기존 문구를 참조하는 테스트 없음을
  test/expected 전수 확인 후 진행.

검증(L40S, pg-cuvs-167-sidecar): installcheck 39/39, isolation 6/6, 배포는
preflight 6개 전부 통과 상태에서.
CAGRA GPU 빌드는 fp32 그대로 두고(cuVS는 fp16 CAGRA 미지원, ADR-054),
write_elem_page()의 마지막 memcpy에서만 halfvec으로 narrow-cast한다.
타겟 컬럼의 실제 카탈로그 타입(atttypid)으로 vector/halfvec을 판별해
bytes_per_dim을 fill_hnsw_from_hnswlib_impl/fill_hnsw_from_cagra_ipc_impl
양쪽에 관통시킨다. halfvec_{l2,ip,cosine}_ops 3종 opclass를 pg_cuvs_hnsw
AM에 등록(0.8.0, SQL-visible 신규 표면이라 마이너 범프). pgvector<0.7.0
+ halfvec 타겟 조합은 ERROR로 조기 차단(halfvec이 없는 버전이라 반드시
실패할 조합).

VM 검증: installcheck 39/39, isolation 6/6 GREEN. 수기 스모크(dim=8,
18벡터, mode=nsw/hnswlib 양쪽) — 검색 정합성(id=1 정확 매치), 재빌드
결정성(155648 bytes 동일) 확인.
기존 check_hnsw_page_fit()은 CAGRA 빌드 + hnswlib 파일 생성이 끝난 뒤에야
실행돼, 거부될 빌드에도 GPU 시간을 다 썼다. cuvs_hnsw_ambuild()와
pg_cuvs_build_hnsw() 양쪽에 동일한 체크를 GPU/IPC 작업 전으로 당겨 추가한다.
maxlevel은 실제 실행 전에는 알 수 없으므로(코퍼스 크기 의존) 고정
safe_maxlevel=5로 다소 보수적으로 거부한다 — 이슈 본문의 방침과 일치.
M은 소스 CAGRA의 graph_degree reloption(없으면 cuVS 기본값 64)에서
유도한다. 기존 사후 체크는 백스톱으로 남긴다.

VM 검증: installcheck 39/39, isolation 6/6 GREEN. dim=1900/M=32/
maxlevel=5(상한 1678) 수동 케이스가 daemon 로그상 export_adjacency
IPC 호출 전에 정확히 거부됨을 확인 — CAGRA 소스 자체 빌드는 별개로
필요하지만(소스 인덱스이므로), HNSW 변환 단계의 GPU 왕복은 스킵됐다.
…AM 경로 halfvec

- test/sql/build_hnsw_halfvec.sql(신규): dim=8/18벡터 end-to-end(mode=nsw/
  hnswlib), index_bytes 재빌드 결정성, dim=3072(이슈의 헤드라인 케이스 —
  vector HNSW 상한 2000으로는 도달 불가) 빌드+검색 정합성.
- build_hnsw_edge.sql Case 6: dim-ceiling 사전 체크가 GPU 작업 전에 실제로
  거부하는지 fp32(dim=1900/M=32)·halfvec(dim=3600/M=32) 양쪽 확인.
- pg_cuvs_hnsw.sql: CREATE INDEX ... USING pg_cuvs_hnsw DDL 경로에 halfvec
  타겟 케이스 1개 추가(기존 3종은 전부 vector).
- Makefile REGRESS에 build_hnsw_halfvec 등록.

새 빌드 호출은 모두 client_min_messages=warning으로 감싸 import NOTICE의
카탈로그 OID가 expected 출력에 박히지 않도록 함(기존 pg_cuvs_hnsw.sql과
동일 패턴) — 처음 작성 시 이 억제가 빠져 OID가 그대로 노출되는 걸 발견,
플레이키해지기 전에 수정.

VM 검증: installcheck 40/40, isolation 6/6 — 2회 연속 GREEN(안정성 확인).
같은 CAGRA 그래프에서 fp32/halfvec 두 HNSW를 export해 fp16 narrow-cast만
격리 측정. wiki_all 768d N=100K를 자기연결로 dim=1536/3072 확장(√m 재정규화
— 순위 보존, 새 GT 계산 불필요, API 비용 0).

dim=1536: recall delta +0.0002(ef=40/100 동일, 2회 재현) — fp16 비용 사실상
무시할 수준. dim=3072: fp32 export 자체가 check_hnsw_page_fit()에서 거부됨
(needed=13736 > 8160) — halfvec만이 유일한 경로이며 recall 0.987~0.998로
정상 동작. #162가 존재하는 이유 자체가 실측으로 확인됨.

정직성 caveat: 자기연결 데이터는 실제 고차원 임베딩보다 판별 정보가 적어
절대 recall 수치는 "실제 1536/3072d recall" 주장이 아님 — paired delta와
dim=3072 buildability 사실만이 유효한 결론.
적대적 코드 리뷰(HIGH 2건, MEDIUM 2건, LOW 다수)에서 발견된 실제 결함 수정:

- [HIGH] safe_maxlevel=5 사전 체크가 mode 무관하게 적용돼 mode='nsw'(기본값,
  실제 max_level 항상 0)의 dim 1679~1918 구간 fp32 빌드를 회귀시켰다 —
  기존에 되던 REINDEX/pg_dump 복원까지 새 .so 로드 순간 깨지는 상태였다.
  이제 mode='nsw'는 정확한 maxlevel=0을, 그 외 모드는 기존 보수적 5를 쓴다.
  build_hnsw_edge.sql의 Case 6(거부 경계)을 실제 상한 밖 dim으로 올리고,
  Case 7로 회귀 자체를 막는 가드(dim=1900 fp32 mode='nsw' 성공 확인) 추가.

- [HIGH] bytes_per_dim_for_typid()가 TypenameGetTypid()로 이름→OID를
  캐시했는데, 이 함수는 호출자의 search_path에 의존한다(PG 소스 확인) —
  제한된 search_path(SECURITY DEFINER, 스키마 한정 설치)에서 평범한 fp32
  빌드까지 스퓨리어스 ERROR가 날 수 있었다. OID→이름 역방향 조회(syscache
  TYPEOID 직접 조회)로 교체 — search_path 의존성 완전 제거, 캐시도 불필요.

- [MEDIUM] pg_cuvs_build_hnsw()의 사전 체크가 cagra AM 검증보다 먼저 실행돼
  cagra가 아닌 인덱스를 넘기면 다른 AM의 옵션 구조체를 CuvsCagraOptions로
  오독할 수 있었다 — 블록 안에 명시적 relam 체크 추가.

- [MEDIUM] 0.8.0 SQL이 halfvec opclass를 무조건 설치해 pgvector<0.7.0에서
  CREATE/ALTER EXTENSION 자체가 실패(fp32 전용 사용자도 영향) — 이미
  도달 불가능해진 require_halfvec_capable_pgvector() C 가드는 삭제하고,
  README 최소 요구사항을 0.7.0+로 명시.

- [LOW] check_hnsw_page_fit()에 dim<=0 가드 추가(부호 없는 랩어라운드로
  체크가 무력화되는 경로 차단). BENCHMARK.md §2.1e의 미검증 "두 번 재현"
  문구와 stale된 사전 체크 에러 수치를 mode-aware 수정 반영 재측정치로
  갱신. pg_cuvs_hnsw.sql의 잘못된 상호참조 주석 수정.

VM 재검증: installcheck 40/40, isolation 6/6 GREEN. dim=1900 fp32
mode='nsw'가 이제 실제로 성공 빌드됨을 확인(회귀 수정 증거).
@ysys143

ysys143 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

적대적 코드 리뷰(에이전트) 결과 및 대응 — 마지막 커밋(04c2f35)에 전부 반영:

HIGH — 실제 회귀, 수정함

  • safe_maxlevel=5 사전 체크가 mode를 무시해 mode='nsw'(기본값)의 fp32 dim 1679~1918 구간을 잘못 거부. mode='nsw'는 실제로 max_level이 항상 0이므로 이제 정확한 경계(maxlevel=0)를 쓰고, 다른 모드는 기존 보수적 5를 유지. 회귀 가드 테스트 추가(dim=1900/mode='nsw' 성공 확인).
  • TypenameGetTypid() 기반 타입 판별이 search_path에 의존해 제한된 search_path에서 평범한 fp32 빌드까지 스퓨리어스 ERROR 가능성. OID→이름 역방향 조회로 교체해 의존성 제거.

MEDIUM — 수정함

  • pg_cuvs_build_hnsw() 사전 체크가 cagra AM 검증보다 먼저 실행되던 순서 버그 — 명시적 relam 체크 추가.
  • 0.8.0 SQL이 halfvec opclass를 무조건 설치해 pgvector<0.7.0에서 설치 자체가 실패 — README 최소 요구사항을 0.7.0+로 명시하고, 이제 도달 불가능해진 C 가드(require_halfvec_capable_pgvector)는 삭제.

LOW — 수정함

  • check_hnsw_page_fit()dim<=0 가드 추가(랩어라운드 방지).
  • BENCHMARK.md의 미검증 "두 번 재현" 문구 제거, mode-aware 수정 반영해 실측치 재측정·갱신.
  • 잘못된 테스트 상호참조 주석 수정.

검증 안 됐다고 확인된 것들 (문제 없음)

  • float_to_half() 손수 구현 — subnormal/carry/부호 경로 전부 IEEE-754와 일치 확인.
  • syscache tuple lifetime — ReleaseSysCache 순서 정확함(추가로 발견된 문제 없음).
  • fresh 0.8.0 vs 0.7.0→0.8.0 업그레이드 카탈로그 동등성 — diff로 확인, 동일함.
  • 잠금 누수 없음, ephemeral 경로의 M=32 가정 유효함(cuvs_wrapper.cu 확인).

VM 재검증: make installcheck 40/40, make installcheck-isolation 6/6, 두 번 연속 GREEN.

ysys143 added 2 commits August 7, 2026 11:04
두 CI 실패 원인:
1. docs-contract-check: src/cuvs_version.h의 PG_CUVS_VERSION이 0.7.0에
   머물러 있었다 — 0.8.0 버전 범프 시 이 SSOT를 빠뜨렸다. control/Makefile은
   갱신했지만 GCS manifest 버전 스탬프용 헤더는 놓침. 함께 docs-contract-check.sh
   자체의 하드코딩된 "현재 버전은 0.7.0" 계약과 release-upgrade.md/
   rollback-and-cleanup.md의 "현재 버전" 실측 예시 출력을 0.8.0으로 갱신
   (이 문서들은 append-only 이력이 아니라 현재 상태를 반영해야 하는 운영
   플레이북 — doc-map.md 기준).

2. Tier-1 CPU shim installcheck: 신규 build_hnsw_halfvec.sql이
   REGRESS_TIER2_ONLY에 빠져 CPU shim(GPU 없음)에서 실행되다 실패했다.
   build_hnsw/build_hnsw_edge/pg_cuvs_hnsw와 동일하게 CAGRA→HNSW GPU
   그래프 export를 쓰는 테스트라 원래 Tier-2 전용이어야 했다 — 목록에 추가.

로컬 검증: scripts/docs-contract-check.sh 통과 확인.
ph_hnsw(vector)/ph_hnsw_solo(vector)는 REINDEX를 명시적으로 테스트했는데
ph_hnsw_hv(halfvec 표현식 인덱스)는 DROP만 하고 REINDEX 경로가 비어 있었다.
표현식 인덱스는 attnum=1의 atttypid가 표현식의 결과 타입(halfvec)으로
카탈로그에 고정되고 REINDEX로도 바뀌지 않으므로 동작해야 한다는 추론은
있었지만 실측이 없었다 — 지금 추가해 확인(REINDEX 후에도 top-1=id=20 정확).

VM 검증: installcheck 40/40, isolation 6/6 GREEN.
@ysys143
ysys143 merged commit 2b1016d into main Aug 7, 2026
2 checks passed
@ysys143
ysys143 deleted the feat/162-halfvec-hnsw-export branch August 7, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: hnsw_import halfvec 지원 — dim=3072 경로 부재 + 1024/1536 인덱스 2~3배

1 participant