tungns2408 commited on
Commit
ec7cb43
·
verified ·
1 Parent(s): 05b48c6

Upload serve_embedding.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. serve_embedding.py +209 -0
serve_embedding.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepX Embedding Server — Deploy on RTX 3060 12GB.
3
+
4
+ Provides REST API for text embedding.
5
+ Loads INT8 quantized model for efficient inference.
6
+
7
+ Usage:
8
+ python scripts/serve_embedding.py \
9
+ --checkpoint checkpoints/deploy/deepx_int8.pt \
10
+ --port 8080
11
+
12
+ API:
13
+ POST /embed
14
+ Body: {"texts": ["text1", "text2", ...], "normalize": true}
15
+ Response: {"embeddings": [[...], [...]], "dim": 1536}
16
+
17
+ POST /similarity
18
+ Body: {"query": "...", "documents": ["doc1", "doc2", ...]}
19
+ Response: {"scores": [0.85, 0.72, ...]}
20
+
21
+ GET /health
22
+ Response: {"status": "ok", "model": "deepx-v0.7", "device": "cuda"}
23
+ """
24
+ import sys, os
25
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
26
+
27
+ import torch
28
+ import torch.nn.functional as F
29
+ import argparse
30
+ import logging
31
+ import time
32
+ from typing import List
33
+ from flask import Flask, request, jsonify
34
+ from transformers import AutoTokenizer
35
+ from config import DeepXConfig
36
+ from modeling.pipeline import DeepXPipeline
37
+
38
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
39
+ logger = logging.getLogger(__name__)
40
+
41
+ app = Flask(__name__)
42
+
43
+ # Globals
44
+ pipeline = None
45
+ tokenizer = None
46
+ device = None
47
+ MAX_LEN = 2048
48
+ MAX_BATCH = 32
49
+
50
+
51
+ def dequantize_int8(state_dict):
52
+ """Dequantize int8 back to float16."""
53
+ dequantized = {}
54
+ scale_keys = {k for k in state_dict if k.endswith("._scale")}
55
+ for key, tensor in state_dict.items():
56
+ if key in scale_keys:
57
+ continue
58
+ scale_key = key + "._scale"
59
+ if tensor.dtype == torch.int8 and scale_key in state_dict:
60
+ scale = state_dict[scale_key].float()
61
+ dequantized[key] = (tensor.float() * scale).half()
62
+ else:
63
+ dequantized[key] = tensor
64
+ return dequantized
65
+
66
+
67
+ def load_model(checkpoint_path, tokenizer_path, device_str="cuda"):
68
+ global pipeline, tokenizer, device
69
+
70
+ device = torch.device(device_str if torch.cuda.is_available() else "cpu")
71
+ logger.info(f"Device: {device}")
72
+
73
+ # Tokenizer
74
+ logger.info(f"Loading tokenizer from {tokenizer_path}...")
75
+ tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
76
+ if tokenizer.pad_token is None:
77
+ tokenizer.pad_token = tokenizer.eos_token
78
+
79
+ # Model
80
+ logger.info(f"Loading model from {checkpoint_path}...")
81
+ config = DeepXConfig()
82
+ pipeline = DeepXPipeline(config, embed_path="pretrained/gemma4_e2b_embed.pt")
83
+
84
+ ckpt = torch.load(checkpoint_path, map_location="cpu")
85
+ sd = ckpt.get("model_state_dict", ckpt)
86
+
87
+ # Dequantize if INT8
88
+ if ckpt.get("quantized", False):
89
+ logger.info("Dequantizing INT8 model...")
90
+ sd = dequantize_int8(sd)
91
+
92
+ pipeline.backbone.load_state_dict(sd, strict=False)
93
+ pipeline = pipeline.to(device).half().eval()
94
+
95
+ # Force pure GDN-2
96
+ for m in pipeline.modules():
97
+ if hasattr(m, 'path_mix_logit'):
98
+ m._alpha_override = 0.0
99
+
100
+ logger.info("Model loaded successfully!")
101
+ logger.info(f" VRAM: {torch.cuda.memory_allocated()/1024**2:.0f} MB" if device.type == "cuda" else " CPU mode")
102
+
103
+
104
+ @torch.no_grad()
105
+ def encode_texts(texts: List[str], normalize: bool = True) -> torch.Tensor:
106
+ """Encode texts to embeddings."""
107
+ # Tokenize
108
+ encoded = tokenizer(
109
+ texts, padding=True, truncation=False, return_tensors="pt", max_length=MAX_LEN
110
+ )
111
+
112
+ # Skip texts too long
113
+ input_ids = encoded["input_ids"]
114
+ attention_mask = encoded["attention_mask"]
115
+
116
+ # Trim to max actual length (save compute)
117
+ max_len = attention_mask.sum(dim=1).max().item()
118
+ max_len = min(max_len, MAX_LEN)
119
+ input_ids = input_ids[:, :max_len].to(device)
120
+ attention_mask = attention_mask[:, :max_len].to(device)
121
+
122
+ # Encode in batches
123
+ all_embs = []
124
+ for i in range(0, len(texts), MAX_BATCH):
125
+ batch_ids = input_ids[i:i+MAX_BATCH]
126
+ batch_mask = attention_mask[i:i+MAX_BATCH]
127
+
128
+ with torch.amp.autocast(device_type="cuda", dtype=torch.float16):
129
+ emb = pipeline(batch_ids, attention_mask=batch_mask, normalize=normalize)
130
+ all_embs.append(emb.cpu())
131
+
132
+ return torch.cat(all_embs)
133
+
134
+
135
+ @app.route("/health", methods=["GET"])
136
+ def health():
137
+ return jsonify({
138
+ "status": "ok",
139
+ "model": "deepx-v0.7-gdn2",
140
+ "device": str(device),
141
+ "max_len": MAX_LEN,
142
+ "dim": 1536,
143
+ })
144
+
145
+
146
+ @app.route("/embed", methods=["POST"])
147
+ def embed():
148
+ data = request.json
149
+ texts = data.get("texts", [])
150
+ normalize = data.get("normalize", True)
151
+
152
+ if not texts:
153
+ return jsonify({"error": "No texts provided"}), 400
154
+ if len(texts) > 100:
155
+ return jsonify({"error": "Max 100 texts per request"}), 400
156
+
157
+ t0 = time.time()
158
+ embeddings = encode_texts(texts, normalize=normalize)
159
+ elapsed = time.time() - t0
160
+
161
+ return jsonify({
162
+ "embeddings": embeddings.tolist(),
163
+ "dim": embeddings.shape[1],
164
+ "count": len(texts),
165
+ "time_ms": round(elapsed * 1000, 1),
166
+ })
167
+
168
+
169
+ @app.route("/similarity", methods=["POST"])
170
+ def similarity():
171
+ data = request.json
172
+ query = data.get("query", "")
173
+ documents = data.get("documents", [])
174
+
175
+ if not query or not documents:
176
+ return jsonify({"error": "Need query and documents"}), 400
177
+
178
+ t0 = time.time()
179
+ all_texts = [query] + documents
180
+ embeddings = encode_texts(all_texts, normalize=True)
181
+
182
+ query_emb = embeddings[0:1]
183
+ doc_embs = embeddings[1:]
184
+ scores = (query_emb @ doc_embs.T).squeeze(0).tolist()
185
+ elapsed = time.time() - t0
186
+
187
+ return jsonify({
188
+ "scores": scores,
189
+ "time_ms": round(elapsed * 1000, 1),
190
+ })
191
+
192
+
193
+ def main():
194
+ parser = argparse.ArgumentParser()
195
+ parser.add_argument("--checkpoint", default="checkpoints/deploy/deepx_int8.pt")
196
+ parser.add_argument("--tokenizer", default="/mnt/d/DX/hf_cache/hub/models--google--gemma-4-E2B-it/snapshots/6b7e72c67d3c4556f42b56d5a68b4b8e864c63b4")
197
+ parser.add_argument("--port", type=int, default=8080)
198
+ parser.add_argument("--host", default="0.0.0.0")
199
+ parser.add_argument("--device", default="cuda")
200
+ args = parser.parse_args()
201
+
202
+ load_model(args.checkpoint, args.tokenizer, args.device)
203
+
204
+ logger.info(f"Starting server on {args.host}:{args.port}")
205
+ app.run(host=args.host, port=args.port, debug=False)
206
+
207
+
208
+ if __name__ == "__main__":
209
+ main()