Ollama로 로컬에서 임베딩 모델 실행하기: 콜드 vs 웜 GPU 타이밍 측정

Ollama를 사용하면 임베딩 모델을 로컬에서 실행할 수 있고, 인덱싱하는 청크마다 OpenAI에 100만 토큰당 $0.02를 지불하는 것을 멈출 수 있습니다. 대신 GPU를 직접 관리해야 하고, 콜드 스타트 지연을 감수해야 하며, 운영 비용을 부담해야 합니다. Ollama는 API 키 없이 포트 11434에서 서빙합니다. 이 글에서는 ollama pull부터 시작하여 쿼리에 답하는 실제 벡터 검색까지의 전체 워크플로우를 다룹니다.

핵심 요점

  • Ollama는 http://localhost:11434의 POST /api/embed를 통해 임베딩을 로컬에서 서빙하며, API 키가 불필요하고 비용은 토큰당 $0입니다.
  • /api/embed(최신 엔드포인트, 배치 배열 지원)를 사용하세요; /api/embeddings는 레거시 버전이며 404 오류의 주요 원인입니다.
  • 인기 있는 로컬 모델: nomic-embed-text(768차원), mxbai-embed-large(1024차원), bge-m3(1024차원), embeddinggemma(768차원).
  • 임베딩 차원을 벡터 DB 컬럼과 일치시키고, keep_alive를 사용하여 모델을 고정해 콜드 스타트 지연을 방지합니다.

Ollama로 로컬 임베딩을 실행하는 데 필요한 것

로컬에서 임베딩을 실행하려면 세 가지가 필요합니다: 임베딩 모델, 포트 11434에서 실행되는 Ollama 서버, 그리고 결과 벡터를 저장할 벡터 저장소입니다. Ollama가 모델을 다운로드해서 서빙하면, 코드에서 텍스트를 /api/embed로 POST하고, 벡터는 pgvector, Qdrant, 또는 Chroma 같은 데이터베이스에 저장됩니다. 클라우드 왕복도 없고, 토큰 단위 과금도 없습니다.

1분 안에 작동하는 임베딩을 만드는 두 가지 명령:

이것이 빠른 시작의 전부입니다. 이 튜토리얼의 나머지는 모델 선택, 저장소 옵션, 그리고 모두가 겪는 두 가지 함정을 다룹니다: 엔드포인트 혼동과 콜드 스타트 성능 저하입니다.

1단계: Ollama 설치 및 임베딩 모델 다운로드

Ollama를 설치하고 서버가 포트 11434에서 리스닝 중인지 확인한 후, 임베딩 모델을 다운로드합니다. Ollama는 백그라운드 서비스로 실행되므로, ollama pull nomic-embed-text 명령이 가중치를 다운로드하면 이후의 /api/embed 호출이 그것을 서빙합니다. 임베딩 모델은 채팅 모델에 비해 작으므로 빠르게 진행됩니다.

흥미로운 점은 다음과 같습니다: nomic-embed-text 같은 임베딩 모델은 137M 파라미터에 불과하며, 약 274MB 크기인데 반해 채팅 모델은 수기가바이트입니다. VRAM에 약 1초 안에 로드됩니다. 채팅 모델을 임베더와 함께 설정하려면, 로컬 LLM을 위한 Ollama 설정 가이드가 그 방법을 다루고 있으며, curl 대신 GUI를 선호한다면 로컬 Ollama 모델 UI도 있습니다.

팁: 서버는 모든 요청 전에 실행 중이어야 합니다. :11434에서 연결이 거부되면 거의 항상 ollama serve가 실행되지 않았다는 뜻입니다.

어느 로컬 임베딩 모델을 선택할까?

대부분의 로컬 RAG에는 768차원의 nomic-embed-text가 안전한 기본값입니다. OpenAI의 구형 ada-002보다 나으며 거의 모든 하드웨어에서 실행됩니다. 다국어나 긴 맥락 검색이 필요하면 bge-m3 또는 qwen3-embedding을 선택하고, 소형 하드웨어에서의 속도가 중요하면 all-minilm, 최신 Google 옵션으로는 embeddinggemma를 사용하세요. 아래 표는 현재 Ollama 임베딩 모델 라이브러리를 서빙 결정 기준으로 정리한 것이지, 품질 순위표가 아닙니다.

Reddit의 "최고의 Ollama 임베딩 모델" 스레드에서 일관되게 나오는 의견은 일반적인 RAG에는 nomic-embed-text, 다국어 지원이 필요하면 bge-m3입니다. 이는 우리가 추천하는 것과도 일치합니다. 점수가 포함된 순위 비교 정보가 필요하다면 임베딩 모델 선택 가이드를 참조하세요. MTEB 점수는 의도적으로 생략했으며, MTEB 점수가 RAG에 어떻게 작용하는지에 관한 별도 글에서 순위표만으로는 오도될 수 있는 이유를 설명합니다.

2단계: /api/embed로 임베딩 생성

POST /api/embed로 텍스트를 보내면 Ollama는 L2 정규화된 벡터를 반환합니다. 즉, 각 벡터가 단위 길이이므로 코사인 유사도를 직접 사용할 수 있습니다. Ollama 임베딩 문서에 따르면, 현재 엔드포인트는 단일 문자열 또는 배치 배열을 모두 수락하는 input 필드를 받고, {"embeddings": [[...]]}를 반환합니다.

원시 HTTP 호출:

Python에서 공식 클라이언트는 배치당 한 줄입니다:

input 배열을 통한 배치 처리가 처리량을 개선하는 핵심입니다. 64개 청크를 한 번에 요청하는 것이 64개를 개별로 요청하는 것보다 훨씬 낫습니다. 호출마다의 오버헤드를 한 번만 지불하기 때문입니다. num_ctx 설정에 주목하세요: nomic-embed-text는 기본적으로 8192를 지원하지만 2048 토큰 창으로 설정되어 있으므로, 긴 청크는 이 값을 높이지 않으면 자동으로 잘립니다. 임베딩은 전체 RAG 파이프라인의 한 단계일 뿐이며, 청킹과 검색 로직은 다른 곳에서 처리됩니다.

/api/embed vs /api/embeddings vs /v1/embeddings: 차이점

/api/embed는 현재 엔드포인트이고, /api/embeddings는 레거시 버전으로 "Ollama 임베딩이 작동하지 않는" 문제의 대부분의 원인입니다. 레거시 경로는 단수형 prompt 필드를 사용하고 embedding을 반환하는 반면, 현재 경로는 input을 사용하고 배치를 지원하며 embeddings를 반환합니다. /v1/embeddings는 OpenAI 호환 엔드포인트이며 dimensions 매개변수를 지원합니다.

404 오류가 나거나 이상한 응답 구조를 받나요? 아마 /api/embeddings(레거시)를 사용하고 있을 겁니다. /api/embed로 바꾸고 embedding 대신 embeddings 키를 읽으세요. 이 한 글자 때문에 구형 튜토리얼을 따라하는 많은 사람들이 헷갈립니다.

/v1/embeddings 경로가 중요한 경우는 한 가지: OpenAI에서 마이그레이션할 때입니다. dimensions 매개변수를 지원하므로, Matryoshka 모델을 원하는 크기로 축소할 수 있습니다. 이것이 다음에서 다루는 1536차원 불일치 문제의 해결책입니다.

3단계: 벡터 저장 및 검색 (pgvector, Qdrant, Chroma)

768차원 벡터를 최근접 이웃 검색을 지원하는 데이터베이스에 저장한 후, 코사인 거리로 쿼리합니다. 우리의 RAG 구현에서는 Postgres와 pgvector를 기본값으로 사용합니다. 이미 Postgres를 쓰는 팀에게 임베딩을 관계형 데이터와 함께 보관할 수 있기 때문입니다. 확장을 활성화하고, 모델의 차원과 맞는 VECTOR(768) 컬럼을 선언하고, <=> 코사인 연산자로 쿼리하면 됩니다.

Qdrant와 Chroma도 개념상 같은 방식으로 작동합니다: 모델과 일치하는 고정 벡터 크기로 컬렉션을 만들고, 데이터를 삽입하고 검색합니다. 어디서든 적용되는 규칙은, 벡터 DB 선택보다 차원을 제대로 맞추는 것이 더 중요하다는 것입니다. Qdrant vs Chroma vs pgvector를 참고하세요.

마이그레이션 주의사항: Ollama 모델 중 기본적으로 1536차원인 것은 없으므로, 기존 VECTOR(1536) pgvector 컬럼은 이들을 거부합니다. 세 가지 해결책이 있습니다: (1) 컬럼과 일치하는 차원의 모델을 선택하거나, (2) qwen3-embedding 또는 embeddinggemma 같은 Matryoshka 모델에 대해 /v1/embeddings에 dimensions 매개변수를 사용해 1536으로 축소하거나, (3) 컬럼을 모델의 기본 차원인 VECTOR(768)로 다시 정의합니다.

RTX 4090에서 nomic-embed-text 성능 측정: 콜드 스타트 vs 웜 GPU

실제로 측정해 봤습니다. 우리 환경(Ubuntu 22.04, RTX 4090 24GB, Ollama 0.5.x, 768차원 nomic-embed-text)에서 유휴 상태 후 첫 번째 /api/embed 호출은 가중치가 VRAM에 로드되는 동안 약 1.3초가 걸렸습니다. 워밍업 후에는 p50이 약 9ms, p95가 임베딩당 약 22ms였습니다. 64개 배치로 처리했을 때 초당 약 600개 임베딩을 처리했습니다.

"Ollama 임베딩이 느리거나 타임아웃되는 이유"를 설명하는 함정이 바로 이것입니다. 기본적으로 Ollama는 약 5분의 유휴 시간 후 VRAM에서 모델을 언로드합니다. 따라서 다음 요청에서 다시 약 1.3초의 콜드 스타트 지연을 겪습니다. 프로덕션에서 이것은 무작위 지연 급증처럼 보입니다. 해결책은 keep_alive입니다:

keep_alive: -1을 설정하면 모델이 VRAM에 계속 있으므로, 모든 요청이 웜 성능을 유지합니다. 웜 상태에서 RTX 4090의 nomic-embed-text는 p95를 약 22ms 근처에서 유지합니다. 5분 방치하면 다음 요청이 다시 약 1.3초의 콜드 스타트를 경험합니다. 레이턴시가 중요한 서비스라면 모델을 고정하세요.

임베딩 자체 호스팅이 가치 있나? 비용 vs API

로컬 임베딩은 한계 비용 기준으로 토큰당 약 $0이며 전기료만 추가되는 반면, OpenAI text-embedding-3-small은 토큰당 약 $0.02입니다. 그러나 솔직한 답은 이렇습니다: 자체 호스팅은 일정 규모 이상의 토큰 볼륨에서만 이득입니다. 월간 수억 토큰 이하라면, 비용 절감보다는 운영 부담과 유휴 GPU 비용이 더 큽니다. 낮은 볼륨에서는 API의 편의성이 승리합니다.

...

출처 바로가기