File size: 6,589 Bytes
ec7cb43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""

DeepX Embedding Server — Deploy on RTX 3060 12GB.



Provides REST API for text embedding.

Loads INT8 quantized model for efficient inference.



Usage:

  python scripts/serve_embedding.py \

    --checkpoint checkpoints/deploy/deepx_int8.pt \

    --port 8080



API:

  POST /embed

  Body: {"texts": ["text1", "text2", ...], "normalize": true}

  Response: {"embeddings": [[...], [...]], "dim": 1536}



  POST /similarity

  Body: {"query": "...", "documents": ["doc1", "doc2", ...]}

  Response: {"scores": [0.85, 0.72, ...]}



  GET /health

  Response: {"status": "ok", "model": "deepx-v0.7", "device": "cuda"}

"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

import torch
import torch.nn.functional as F
import argparse
import logging
import time
from typing import List
from flask import Flask, request, jsonify
from transformers import AutoTokenizer
from config import DeepXConfig
from modeling.pipeline import DeepXPipeline

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

app = Flask(__name__)

# Globals
pipeline = None
tokenizer = None
device = None
MAX_LEN = 2048
MAX_BATCH = 32


def dequantize_int8(state_dict):
    """Dequantize int8 back to float16."""
    dequantized = {}
    scale_keys = {k for k in state_dict if k.endswith("._scale")}
    for key, tensor in state_dict.items():
        if key in scale_keys:
            continue
        scale_key = key + "._scale"
        if tensor.dtype == torch.int8 and scale_key in state_dict:
            scale = state_dict[scale_key].float()
            dequantized[key] = (tensor.float() * scale).half()
        else:
            dequantized[key] = tensor
    return dequantized


def load_model(checkpoint_path, tokenizer_path, device_str="cuda"):
    global pipeline, tokenizer, device
    
    device = torch.device(device_str if torch.cuda.is_available() else "cpu")
    logger.info(f"Device: {device}")
    
    # Tokenizer
    logger.info(f"Loading tokenizer from {tokenizer_path}...")
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    
    # Model
    logger.info(f"Loading model from {checkpoint_path}...")
    config = DeepXConfig()
    pipeline = DeepXPipeline(config, embed_path="pretrained/gemma4_e2b_embed.pt")
    
    ckpt = torch.load(checkpoint_path, map_location="cpu")
    sd = ckpt.get("model_state_dict", ckpt)
    
    # Dequantize if INT8
    if ckpt.get("quantized", False):
        logger.info("Dequantizing INT8 model...")
        sd = dequantize_int8(sd)
    
    pipeline.backbone.load_state_dict(sd, strict=False)
    pipeline = pipeline.to(device).half().eval()
    
    # Force pure GDN-2
    for m in pipeline.modules():
        if hasattr(m, 'path_mix_logit'):
            m._alpha_override = 0.0
    
    logger.info("Model loaded successfully!")
    logger.info(f"  VRAM: {torch.cuda.memory_allocated()/1024**2:.0f} MB" if device.type == "cuda" else "  CPU mode")


@torch.no_grad()
def encode_texts(texts: List[str], normalize: bool = True) -> torch.Tensor:
    """Encode texts to embeddings."""
    # Tokenize
    encoded = tokenizer(
        texts, padding=True, truncation=False, return_tensors="pt", max_length=MAX_LEN
    )
    
    # Skip texts too long
    input_ids = encoded["input_ids"]
    attention_mask = encoded["attention_mask"]
    
    # Trim to max actual length (save compute)
    max_len = attention_mask.sum(dim=1).max().item()
    max_len = min(max_len, MAX_LEN)
    input_ids = input_ids[:, :max_len].to(device)
    attention_mask = attention_mask[:, :max_len].to(device)
    
    # Encode in batches
    all_embs = []
    for i in range(0, len(texts), MAX_BATCH):
        batch_ids = input_ids[i:i+MAX_BATCH]
        batch_mask = attention_mask[i:i+MAX_BATCH]
        
        with torch.amp.autocast(device_type="cuda", dtype=torch.float16):
            emb = pipeline(batch_ids, attention_mask=batch_mask, normalize=normalize)
        all_embs.append(emb.cpu())
    
    return torch.cat(all_embs)


@app.route("/health", methods=["GET"])
def health():
    return jsonify({
        "status": "ok",
        "model": "deepx-v0.7-gdn2",
        "device": str(device),
        "max_len": MAX_LEN,
        "dim": 1536,
    })


@app.route("/embed", methods=["POST"])
def embed():
    data = request.json
    texts = data.get("texts", [])
    normalize = data.get("normalize", True)
    
    if not texts:
        return jsonify({"error": "No texts provided"}), 400
    if len(texts) > 100:
        return jsonify({"error": "Max 100 texts per request"}), 400
    
    t0 = time.time()
    embeddings = encode_texts(texts, normalize=normalize)
    elapsed = time.time() - t0
    
    return jsonify({
        "embeddings": embeddings.tolist(),
        "dim": embeddings.shape[1],
        "count": len(texts),
        "time_ms": round(elapsed * 1000, 1),
    })


@app.route("/similarity", methods=["POST"])
def similarity():
    data = request.json
    query = data.get("query", "")
    documents = data.get("documents", [])
    
    if not query or not documents:
        return jsonify({"error": "Need query and documents"}), 400
    
    t0 = time.time()
    all_texts = [query] + documents
    embeddings = encode_texts(all_texts, normalize=True)
    
    query_emb = embeddings[0:1]
    doc_embs = embeddings[1:]
    scores = (query_emb @ doc_embs.T).squeeze(0).tolist()
    elapsed = time.time() - t0
    
    return jsonify({
        "scores": scores,
        "time_ms": round(elapsed * 1000, 1),
    })


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--checkpoint", default="checkpoints/deploy/deepx_int8.pt")
    parser.add_argument("--tokenizer", default="/mnt/d/DX/hf_cache/hub/models--google--gemma-4-E2B-it/snapshots/6b7e72c67d3c4556f42b56d5a68b4b8e864c63b4")
    parser.add_argument("--port", type=int, default=8080)
    parser.add_argument("--host", default="0.0.0.0")
    parser.add_argument("--device", default="cuda")
    args = parser.parse_args()
    
    load_model(args.checkpoint, args.tokenizer, args.device)
    
    logger.info(f"Starting server on {args.host}:{args.port}")
    app.run(host=args.host, port=args.port, debug=False)


if __name__ == "__main__":
    main()