상태: 초안 (2026-11-15), 실측. 이 글의 숫자는 2026-11-15 에 필자가 직접 잰 값입니다. 쿠버네티스 v1.34, Dynamo v1.6.2 의
vllm-runtime:1.6.2이미지(vLLM 0.31.0), 모델 Qwen3-Embedding-0.6B, 8GB급 노트북 GPU 한 장입니다. 견준 상대는 범용 제품이 아니라 필자가 직접 만든 서버 하나이므로, 숫자를 다른 서버에 그대로 옮기면 안 됩니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
임베딩 서버를 Dynamo 로 옮겨 보기
목차
1. 왜 임베딩인가
3편과 4편은 시험용 채팅 모델로 쟀습니다. 이번에는 실제로 쓰고 있는 서비스에 Dynamo 를 대 봤습니다. 그런데 쓰고 있는 서비스를 들여다보니 채팅 모델이 아니었습니다. 검색(RAG)에 쓰는 임베딩과 재순위, 그리고 음성 인식을 한 프로세스가 GPU 한 장에서 내주는, 직접 만든 FastAPI 서버였습니다.
이 서버에는 Dynamo 의 대표 기능이 쓸모가 없습니다. 임베딩은 토큰을 생성하지 않으므로 접두 캐시도, KV 라우팅도, 분리 서빙도 해당하지 않습니다. Dynamo 의 코드도 같은 말을 합니다. 임베딩 워커의 도움말에는 KV 이벤트와 KV 라우터 등록을 건너뛴다고 적혀 있습니다(v1.6.2, components/src/dynamo/vllm/backend_args.py).
그래서 물음이 바뀝니다. 직접 만든 서버를 걷어 내고 Dynamo 임베딩 워커로 바꿔 끼울 수 있는가, 그리고 그렇게 하면 무엇이 달라지는가입니다.
2. 구성
Dynamo 의 vLLM 백엔드는 --embedding-worker 인자로 임베딩 전용 워커가 됩니다. vLLM 쪽에는 --runner pooling 을 함께 줍니다.
command: ["python3", "-m", "dynamo.vllm"]
args:
- --model
- Qwen/Qwen3-Embedding-0.6B
- --embedding-worker
- --runner
- pooling
- --max-model-len
- "1024"
- --gpu-memory-utilization
- "0.4"
그래프는 프런트엔드 하나와 워커 하나입니다. 모델은 기존 서버가 쓰는 것과 같은 리비전을 미리 받아 두고, 워커는 HF_HUB_OFFLINE=1 로 띄웠습니다. --max-model-len 1024 는 기존 서버의 입력 한도와 맞춘 값입니다.
견준 순서는 이렇습니다.
- 기존 서버에 요청을 보내 벡터와 지연 시간을 모읍니다(3회).
- 기존 서버를 내리고, 같은 GPU 에 Dynamo 그래프를 올립니다.
- 같은 요청을 Dynamo 프런트엔드에 보냅니다(3회).
- 그래프를 내리고 기존 서버를 되살립니다.
요청은 문서 40개(한국어와 영어, 문서 하나에 평균 84토큰)를 8개씩 묶은 것입니다. 기존 서버의 클라이언트가 실제로 8개씩 묶어 보내기 때문입니다. 지연 시간은 순차 30회와 동시 4 로 40회를 쟀고, 3회 반복의 중앙값을 씁니다.
3. 벡터는 같았다
| 항목 | 기존 서버 | Dynamo 임베딩 워커 |
|---|---|---|
| 차원 | 1024 | 1024 |
| 첫 벡터의 길이(L2) | 1.0 | 1.0 |
| 문서 40개의 토큰 수 합 | 3,375 | 3,375 |
| 문서별 코사인 유사도 | 최소 0.9996 · 중앙 0.9998 · 최대 0.9999 |
토큰 수가 정확히 같고, 벡터는 소수 넷째 자리에서 갈립니다. 기존 서버는 transformers 로 직접 추론하고 Dynamo 쪽은 vLLM 이 추론하므로 연산 순서와 정밀도 처리가 다릅니다. 검색 순위를 바꿀 만한 차이인지는 따로 재지 않았습니다(미실측). 색인을 다시 만들지 않고 섞어 써도 되는지는 이 숫자만으로 단정하지 않습니다.
4. 그대로는 바꿔 끼울 수 없었다
두 서버 모두 /v1/embeddings 를 엽니다. 이름이 같아서 주소만 바꾸면 될 것처럼 보이지만, 같은 요청에 다르게 답한 것이 넷이었습니다.
| 요청 | 기존 서버 | Dynamo 임베딩 워커 |
|---|---|---|
dimensions: 256 |
200, 256차원 | 500 |
input_type: "query" (질의용 지시문) |
200, 지시문을 붙여 28토큰 | 200, 무시하고 9토큰 |
| 한도를 넘는 긴 입력 | 200, 1,024토큰으로 자름 | 500 |
encoding_format: "base64" |
무시하고 배열 | base64 문자열 |
dimensions. 워커 로그에 까닭이 남았습니다. vLLM 이 이 모델을 Matryoshka 임베딩을 지원하지 않는 모델로 보고, dimensions 가 있으면 요청을 거절합니다. 기존 클라이언트는 모든 요청에 dimensions 를 넣어 보냅니다. 값이 모델의 원래 차원과 같아도 「없어야 한다」는 검사에 걸리므로, 클라이언트를 고치지 않으면 요청이 전부 실패합니다. vLLM 에는 모델을 Matryoshka 로 다루게 하는 재정의 옵션이 있다고 알고 있으나 이 글에서는 시험하지 않았습니다(미실측).
질의용 지시문. 이쪽이 더 위험합니다. 오류가 나지 않기 때문입니다. Qwen3-Embedding 은 질의 앞에 지시문을 붙이도록 만든 모델이고, 기존 서버는 input_type 이나 instruction 필드를 받아 그 일을 대신 합니다. Dynamo 쪽은 모르는 필드를 버리고 글자 그대로 임베딩합니다. 같은 질의로 양쪽이 낸 벡터의 코사인 유사도는 0.82 였습니다. 지시문 없이 견주면 0.9995 입니다. 주소만 바꾸면 모든 요청이 200 으로 돌아오는데 검색 품질은 조용히 떨어집니다. 지시문은 클라이언트가 글자로 붙여서 보내야 합니다.
긴 입력. 기존 서버는 한도에서 자르고, vLLM 은 한도를 넘으면 거절합니다. 어느 쪽이 옳다고 하기는 어렵습니다. 다만 클라이언트가 자르기를 서버에 맡겨 왔다면 그 일을 가져와야 합니다.
오류 코드. dimensions 와 긴 입력은 둘 다 클라이언트의 요청이 잘못된 경우인데, 프런트엔드는 Internal server error 와 함께 500 을 돌려줬습니다. 워커 로그에는 vLLM 의 검증 오류가 까닭과 함께 남아 있었습니다. 500 은 재시도 대상으로 다루는 클라이언트가 많으므로, 이대로면 고칠 수 없는 요청을 계속 다시 보내게 됩니다. v1.6.2 에서 본 동작이고, 최신 판에서 바뀌었는지는 미확인입니다.
5. 지연 시간과 처리량
같은 GPU(8GB급) 에서 잰 값입니다. 묶음 8개 요청 하나의 지연 시간이고, 3회 반복의 중앙값입니다. 오류는 양쪽 모두 0건이었습니다.
| 순차 p50 | 순차 p99 | 동시 4 p50 | 동시 4 p99 | 동시 4 처리량 | |
|---|---|---|---|---|---|
| 기존 서버 | 54.3ms | 76.0ms | 225.9ms | 245.9ms | 141.0문서/s |
| Dynamo 임베딩 워커 | 36.9ms | 40.5ms | 77.4ms | 108.9ms | 395.8문서/s |
순차 지연은 0.68배, 동시 4 의 처리량은 2.8배입니다. 반복별 처리량은 기존 서버가 139.4·141.0·141.2, Dynamo 가 409.1·395.8·351.9 문서/s 였습니다.
동시 4 에서 벌어지는 까닭은 기존 서버의 구조로 보입니다. 기존 서버는 추론을 전역 잠금 하나로 감싸서 한 번에 한 요청만 처리합니다. 동시 4 의 지연(226ms)이 순차(54ms)의 약 네 배인 것이 그 흔적입니다. vLLM 은 동시에 들어온 요청을 한 배치로 묶습니다. 다만 잠금을 풀거나 배치를 끄고 따로 잰 것은 아니므로, 이 설명은 확인한 것이 아니라 숫자에 맞는 추정입니다.
이 표를 읽을 때 조심할 것이 셋 있습니다.
- 조건이 완전히 같지 않습니다. 기존 서버는 같은 카드에 재순위 모델과 음성 인식 모델도 올려 둡니다(카드 사용량 3.7GB). Dynamo 워커에는 임베딩 모델 하나만 올렸습니다(3.1GB).
- 기존 서버에는 실제 트래픽이 섞였을 수 있습니다. 재는 동안 다른 요청을 막지 않았습니다.
- 견준 상대가 직접 만든 서버입니다. 「Dynamo 가 2.8배 빠르다」가 아니라 「요청을 하나씩 처리하는 서버를 배치하는 엔진으로 바꾸면 이만큼 달라진다」로 읽어야 합니다. 같은 차이는 Dynamo 없이 vLLM 만 올려도 날 가능성이 큽니다. vLLM 단독과의 대조는 하지 않았습니다(미실측).
6. 옮기지 못하는 것
기존 서버가 하는 일은 셋이고, 이 글이 옮겨 본 것은 하나입니다.
| 기능 | Dynamo v1.6.2 의 vLLM 백엔드 | 이 글에서 |
|---|---|---|
| 임베딩 | --embedding-worker |
실측 |
| 재순위 | 재순위 핸들러는 SGLang 백엔드 쪽 코드에서만 찾았습니다 | 미실측 |
| 음성 인식 | 실시간 전사 핸들러가 코드에 있습니다 | 미실측 |
임베딩만 옮기면 서버가 하나에서 둘로 늘어납니다. 기존 서버는 세 모델을 한 프로세스에 올려 8GB 카드 하나를 나눠 쓰는데, 임베딩을 따로 떼면 그 카드에 프로세스가 둘이 되고 프런트엔드 파드도 하나 더 생깁니다. 워커가 한 대뿐이라면 Dynamo 프런트엔드가 해 주는 일은 요청을 그 한 대로 넘기는 것뿐입니다.
7. 정리
- Dynamo 의 임베딩 워커는 인자 둘(
--embedding-worker,--runner pooling)로 떴고, 같은 모델에서 기존 서버와 같은 벡터를 냈습니다(코사인 최소 0.9995). - 같은 GPU 에서 순차 지연은 54ms 에서 37ms 로, 동시 4 의 처리량은 141문서/s 에서 396문서/s 로 바뀌었습니다. 차이의 대부분은 요청을 하나씩 처리하는 구조와 배치하는 구조의 차이로 보입니다.
- 주소만 바꾸는 교체는 실패합니다.
dimensions는 500 이 되고, 질의용 지시문은 오류 없이 무시되어 벡터가 달라지며(코사인 0.82), 긴 입력은 잘리지 않고 500 이 됩니다. - 옮기기 전에 할 일은 클라이언트에 있습니다.
dimensions를 빼고, 지시문을 글자로 붙이고, 길이를 미리 자르고, 500 을 재시도하지 않게 해야 합니다. - 워커가 한 대인 임베딩 서비스에서 Dynamo 가 더해 주는 것은 적습니다. 얻은 이득은 엔진을 vLLM 으로 바꾼 데서 왔을 가능성이 크고, 그것은 Dynamo 없이도 할 수 있는 일입니다.
시리즈의 앞 편들과 이어 보면 이렇습니다. KV 라우팅은 접두가 겹치는 채팅 요청에서 값을 합니다(3편). 분리 서빙은 빠른 망과 큰 모델이 있어야 합니다(4편). 임베딩처럼 생성이 없는 일에는 둘 다 해당하지 않고, 남는 물음은 API 계약이 맞는가입니다(이 편).
참고
- NVIDIA Dynamo v1.6.2,
components/src/dynamo/vllm/backend_args.py의--embedding-worker도움말과embedding_worker_processes.py. - vLLM 문서, Pooling Models 와 OpenAI 호환 Embeddings API.
- Qwen3-Embedding 모델 카드의 지시문(instruction) 사용법.
- 매니페스트, 대조 스크립트(
embed_probe.py), 중앙값 요약은 저장소의docs/experiments/2026-11-15-dynamo-embedding/에 있습니다.