Skip to content

Commit a53d357

Browse files
Refine CLIP embedding (#1245)
* Refine clip embedding Signed-off-by: lvliang-intel <[email protected]>
1 parent 63c66a0 commit a53d357

File tree

9 files changed

+312
-4
lines changed

9 files changed

+312
-4
lines changed

comps/embeddings/deployment/docker_compose/compose.yaml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ services:
2626
image: ${REGISTRY:-opea}/embedding:${TAG:-latest}
2727
container_name: tei-embedding-server
2828
ports:
29-
- "${EMBEDDER_PORT:-10200}:6000"
29+
- "${EMBEDDER_PORT:-10201}:6000"
3030
ipc: host
3131
environment:
3232
no_proxy: ${no_proxy}
@@ -43,7 +43,7 @@ services:
4343
image: ${REGISTRY:-opea}/embedding:${TAG:-latest}
4444
container_name: pg-embedding-server
4545
ports:
46-
- ${EMBEDDER_PORT:-10200}:6000
46+
- ${EMBEDDER_PORT:-10202}:6000
4747
ipc: host
4848
environment:
4949
no_proxy: ${no_proxy}
@@ -54,6 +54,19 @@ services:
5454
EMBEDDING_COMPONENT_NAME: "OPEA_PREDICTIONGUARD_EMBEDDING"
5555
restart: unless-stopped
5656

57+
clip-embedding-server:
58+
image: ${REGISTRY:-opea}/embedding:${TAG:-latest}
59+
container_name: clip-embedding-server
60+
ports:
61+
- ${EMBEDDER_PORT:-10203}:6000
62+
ipc: host
63+
environment:
64+
no_proxy: ${no_proxy}
65+
http_proxy: ${http_proxy}
66+
https_proxy: ${https_proxy}
67+
EMBEDDING_COMPONENT_NAME: "OPEA_CLIP_EMBEDDING"
68+
restart: unless-stopped
69+
5770
multimodal-bridgetower-embedding-server:
5871
<<: *multimodal-bridgetower-embedding-config
5972
depends_on:

comps/embeddings/src/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ For details, please refer to [readme](./README_tei.md).
2222

2323
For details, please refer to this [readme](./README_predictionguard.md).
2424

25+
## Embeddings Microservice with Multimodal Clip
26+
27+
For details, please refer to this [readme](./README_clip.md)
28+
2529
## Embeddings Microservice with Multimodal
2630

2731
For details, please refer to this [readme](./README_bridgetower.md).
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Multimodal CLIP Embedding Microservice
2+
3+
The Multimodal CLIP Embedding Microservice provides a powerful solution for converting textual and visual data into high-dimensional vector embeddings. These embeddings capture the semantic essence of the input, enabling robust applications in multi-modal data processing, information retrieval, recommendation systems, and more.
4+
5+
## ✨ Key Features
6+
7+
- **High Performance**: Optimized for rapid and reliable embedding generation for text and images.
8+
- **Scalable**: Capable of handling high-concurrency workloads, ensuring consistent performance under heavy loads.
9+
- **Easy Integration**: Offers a simple API interface for seamless integration into diverse workflows.
10+
- **Customizable**: Supports tailored configurations, including model selection and preprocessing adjustments, to fit specific requirements.
11+
12+
This service empowers users to configure and deploy embedding pipelines tailored to their needs.
13+
14+
---
15+
16+
## 🚀 Quick Start
17+
18+
### 1. Launch the Microservice with Docker
19+
20+
#### 1.1 Build the Docker Image
21+
22+
To build the Docker image, execute the following commands:
23+
24+
```bash
25+
cd ../../..
26+
docker build -t opea/embedding:latest \
27+
--build-arg https_proxy=$https_proxy \
28+
--build-arg http_proxy=$http_proxy \
29+
-f comps/embeddings/src/Dockerfile .
30+
```
31+
32+
#### 1.2 Start the Service with Docker Compose
33+
34+
Use Docker Compose to start the service:
35+
36+
```bash
37+
cd comps/embeddings/deployment/docker_compose/
38+
docker compose up clip-embedding-server -d
39+
```
40+
41+
---
42+
43+
### 2. Consume the Embedding Service
44+
45+
#### 2.1 Check Service Health
46+
47+
Verify that the service is running by performing a health check:
48+
49+
```bash
50+
curl http://localhost:6000/v1/health_check \
51+
-X GET \
52+
-H 'Content-Type: application/json'
53+
```
54+
55+
#### 2.2 Generate Embeddings
56+
57+
The service supports [OpenAI API](https://platform.openai.com/docs/api-reference/embeddings)-compatible requests.
58+
59+
- **Single Text Input**:
60+
61+
```bash
62+
curl http://localhost:6000/v1/embeddings \
63+
-X POST \
64+
-d '{"input":"Hello, world!"}' \
65+
-H 'Content-Type: application/json'
66+
```
67+
68+
- **Multiple Texts with Parameters**:
69+
70+
```bash
71+
curl http://localhost:6000/v1/embeddings \
72+
-X POST \
73+
-d '{"input":["Hello, world!","How are you?"], "dimensions":100}' \
74+
-H 'Content-Type: application/json'
75+
```

comps/embeddings/src/README_tei.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ This guide walks you through starting, deploying, and consuming the **TEI-based
4444

4545
```bash
4646
docker run -d --name="embedding-tei-server" \
47-
-p 6000:5000 \
47+
-p 6000:6000 \
4848
-e http_proxy=$http_proxy -e https_proxy=$https_proxy \
4949
--ipc=host \
5050
-e TEI_EMBEDDING_ENDPOINT=$TEI_EMBEDDING_ENDPOINT \
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright (C) 2024 Intel Corporation
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import os
5+
from typing import List, Union
6+
7+
import torch
8+
import torch.nn as nn
9+
from einops import rearrange
10+
from transformers import AutoProcessor, AutoTokenizer, CLIPModel
11+
12+
from comps import CustomLogger, OpeaComponent, OpeaComponentRegistry, ServiceType
13+
from comps.cores.proto.api_protocol import EmbeddingRequest, EmbeddingResponse, EmbeddingResponseData
14+
15+
logger = CustomLogger("opea_multimodal_embedding_clip")
16+
logflag = os.getenv("LOGFLAG", False)
17+
18+
19+
model_name = "openai/clip-vit-base-patch32"
20+
21+
clip = CLIPModel.from_pretrained(model_name)
22+
processor = AutoProcessor.from_pretrained(model_name)
23+
tokenizer = AutoTokenizer.from_pretrained(model_name)
24+
25+
26+
class vCLIP(nn.Module):
27+
def __init__(self, cfg):
28+
super().__init__()
29+
30+
self.num_frm = cfg["num_frm"]
31+
self.model_name = cfg["model_name"]
32+
33+
def embed_query(self, texts):
34+
"""Input is list of texts."""
35+
text_inputs = tokenizer(texts, padding=True, return_tensors="pt")
36+
text_features = clip.get_text_features(**text_inputs)
37+
return text_features
38+
39+
def get_embedding_length(self):
40+
text_features = self.embed_query("sample_text")
41+
return text_features.shape[1]
42+
43+
def get_image_embeddings(self, images):
44+
"""Input is list of images."""
45+
image_inputs = processor(images=images, return_tensors="pt")
46+
image_features = clip.get_image_features(**image_inputs)
47+
return image_features
48+
49+
def get_video_embeddings(self, frames_batch):
50+
"""Input is list of list of frames in video."""
51+
self.batch_size = len(frames_batch)
52+
vid_embs = []
53+
for frames in frames_batch:
54+
frame_embeddings = self.get_image_embeddings(frames)
55+
frame_embeddings = rearrange(frame_embeddings, "(b n) d -> b n d", b=len(frames_batch))
56+
# Normalize, mean aggregate and return normalized video_embeddings
57+
frame_embeddings = frame_embeddings / frame_embeddings.norm(dim=-1, keepdim=True)
58+
video_embeddings = frame_embeddings.mean(dim=1)
59+
video_embeddings = video_embeddings / video_embeddings.norm(dim=-1, keepdim=True)
60+
vid_embs.append(video_embeddings)
61+
return torch.cat(vid_embs, dim=0)
62+
63+
64+
@OpeaComponentRegistry.register("OPEA_CLIP_EMBEDDING")
65+
class OpeaClipEmbedding(OpeaComponent):
66+
"""A specialized embedding component derived from OpeaComponent for CLIP embedding services.
67+
68+
This class initializes and configures the CLIP embedding service using the vCLIP model.
69+
It also performs a health check during initialization and logs an error if the check fails.
70+
71+
Attributes:
72+
embeddings (vCLIP): An instance of the vCLIP model used for generating embeddings.
73+
"""
74+
75+
def __init__(self, name: str, description: str, config: dict = None):
76+
super().__init__(name, ServiceType.EMBEDDING.name.lower(), description, config)
77+
self.embeddings = vCLIP({"model_name": "openai/clip-vit-base-patch32", "num_frm": 4})
78+
79+
health_status = self.check_health()
80+
if not health_status:
81+
logger.error("OpeaClipEmbedding health check failed.")
82+
83+
async def invoke(self, input: EmbeddingRequest) -> EmbeddingResponse:
84+
"""Invokes the embedding service to generate embeddings for the provided input.
85+
86+
Args:
87+
input (EmbeddingRequest): The input in OpenAI embedding format, including text(s) and optional parameters like model.
88+
89+
Returns:
90+
EmbeddingResponse: The response in OpenAI embedding format, including embeddings, model, and usage information.
91+
"""
92+
# Parse input according to the EmbeddingRequest format
93+
if isinstance(input.input, str):
94+
texts = [input.input.replace("\n", " ")]
95+
elif isinstance(input.input, list):
96+
if all(isinstance(item, str) for item in input.input):
97+
texts = [text.replace("\n", " ") for text in input.input]
98+
else:
99+
raise ValueError("Invalid input format: Only string or list of strings are supported.")
100+
else:
101+
raise TypeError("Unsupported input type: input must be a string or list of strings.")
102+
embed_vector = self.get_embeddings(texts)
103+
if input.dimensions is not None:
104+
embed_vector = [embed_vector[i][: input.dimensions] for i in range(len(embed_vector))]
105+
106+
# for standard openai embedding format
107+
res = EmbeddingResponse(
108+
data=[EmbeddingResponseData(index=i, embedding=embed_vector[i]) for i in range(len(embed_vector))]
109+
)
110+
return res
111+
112+
def check_health(self) -> bool:
113+
"""Checks if the embedding model is healthy.
114+
115+
Returns:
116+
bool: True if the embedding model is initialized, False otherwise.
117+
"""
118+
if self.embeddings:
119+
return True
120+
else:
121+
return False
122+
123+
def get_embeddings(self, text: Union[str, List[str]]) -> List[List[float]]:
124+
"""Generates embeddings for input text.
125+
126+
Args:
127+
text (Union[str, List[str]]): Input text or list of texts.
128+
129+
Returns:
130+
List[List[float]]: List of embedding vectors.
131+
"""
132+
texts = [text] if isinstance(text, str) else text
133+
embed_vector = self.embeddings.embed_query(texts).tolist()
134+
return embed_vector

comps/embeddings/src/opea_embedding_microservice.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import time
66

7+
from integrations.clip import OpeaClipEmbedding
78
from integrations.predictionguard import PredictionguardEmbedding
89
from integrations.tei import OpeaTEIEmbedding
910

comps/embeddings/src/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
aiohttp
22
docarray
3+
einops
34
fastapi
45
huggingface_hub
6+
open-clip-torch
57
openai
68
opentelemetry-api
79
opentelemetry-exporter-otlp
@@ -11,4 +13,5 @@ predictionguard==2.2.1
1113
prometheus-fastapi-instrumentator
1214
PyYAML
1315
shortuuid
16+
transformers
1417
uvicorn
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/bin/bash
2+
# Copyright (C) 2024 Intel Corporation
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
set -x
6+
7+
WORKPATH=$(dirname "$PWD")
8+
ip_address=$(hostname -I | awk '{print $1}')
9+
10+
function build_docker_images() {
11+
cd $WORKPATH
12+
echo $(pwd)
13+
docker build --no-cache -t opea/embedding:comps --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/embeddings/src/Dockerfile .
14+
if [ $? -ne 0 ]; then
15+
echo "opea/embedding built fail"
16+
exit 1
17+
else
18+
echo "opea/embedding built successful"
19+
fi
20+
}
21+
22+
function start_service() {
23+
export TAG=comps
24+
export host_ip=${ip_address}
25+
export EMBEDDER_PORT=10203
26+
service_name="clip-embedding-server"
27+
cd $WORKPATH
28+
cd comps/embeddings/deployment/docker_compose/
29+
docker compose up ${service_name} -d
30+
sleep 15
31+
}
32+
33+
function validate_service() {
34+
local INPUT_DATA="$1"
35+
service_port=10203
36+
result=$(http_proxy="" curl http://${ip_address}:$service_port/v1/embeddings \
37+
-X POST \
38+
-d "$INPUT_DATA" \
39+
-H 'Content-Type: application/json')
40+
if [[ $result == *"embedding"* ]]; then
41+
echo "Result correct."
42+
else
43+
echo "Result wrong. Received was $result"
44+
docker logs clip-embedding-server
45+
exit 1
46+
fi
47+
}
48+
49+
function validate_microservice() {
50+
## Test OpenAI API, input single text
51+
validate_service \
52+
'{"input":"What is Deep Learning?"}'
53+
54+
## Test OpenAI API, input multiple texts with parameters
55+
validate_service \
56+
'{"input":["What is Deep Learning?","How are you?"], "dimensions":100}'
57+
}
58+
59+
function stop_docker() {
60+
cid=$(docker ps -aq --filter "name=clip-embedding-server*")
61+
if [[ ! -z "$cid" ]]; then docker stop $cid && docker rm $cid && sleep 1s; fi
62+
}
63+
64+
function main() {
65+
66+
stop_docker
67+
68+
build_docker_images
69+
start_service
70+
71+
validate_microservice
72+
73+
stop_docker
74+
echo y | docker system prune
75+
76+
}
77+
78+
main

tests/embeddings/test_embeddings_predictionguard.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function start_service() {
3030
cd $WORKPATH
3131
cd comps/embeddings/deployment/docker_compose/
3232
docker compose up ${service_name} -d
33-
sleep 10
33+
sleep 30
3434
}
3535

3636
function validate_service() {

0 commit comments

Comments
 (0)