Agent 2.0 exdev 서버 배포 스택
- server-dev start/stop/deploy 및 Gitea push 자동 배포 - local-dev 로컬 개발 환경 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
=========== embedding ===========
|
||||
|
||||
docker run -d --gpus all \
|
||||
--name tei-embedding \
|
||||
-p 16001:80 \
|
||||
-v /DATA/exlink/models:/data \
|
||||
--pull never \
|
||||
--restart always \
|
||||
ghcr.io/huggingface/text-embeddings-inference:hopper-1.8 \
|
||||
--model-id /data/qwen3-embedding-8b \
|
||||
--port 80 \
|
||||
--max-batch-tokens 40960
|
||||
|
||||
|
||||
curl 127.0.0.1:16001/embed \
|
||||
-X POST \
|
||||
-d '{"inputs":"안녕하세요, 임베딩 테스트 중입니다."}' \
|
||||
-H 'Content-Type: application/json'
|
||||
=========== embedding ===========
|
||||
|
||||
=========== reranker ===========
|
||||
|
||||
docker rm -f vllm-reranker
|
||||
|
||||
docker rm -f vllm-reranker
|
||||
|
||||
docker run -d --gpus all \
|
||||
--name vllm-reranker \
|
||||
-p 16002:8000 \
|
||||
-v /DATA/exlink/models:/data \
|
||||
--ipc=host \
|
||||
--restart always \
|
||||
-e HF_HUB_OFFLINE=1 \
|
||||
-e TRANSFORMERS_OFFLINE=1 \
|
||||
vllm/vllm-openai:latest \
|
||||
python3 /data/reranker_server.py
|
||||
|
||||
curl http://127.0.0.1:16002/v1/rerank \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "/data/qwen3-reranker-4b",
|
||||
"query": "사과는 어떤 과일인가요?",
|
||||
"documents": [
|
||||
"사과는 빨간색 과일이며 비타민이 풍부합니다.",
|
||||
"서울은 대한민국의 수도입니다."
|
||||
]
|
||||
}'
|
||||
=========== reranker ===========
|
||||
|
||||
|
||||
=========== LLM ===========
|
||||
|
||||
docker run -d --gpus all \
|
||||
--name vllm-server \
|
||||
--memory 32g \
|
||||
-p 16000:8000 \
|
||||
-v /DATA/exlink/models:/data \
|
||||
--ipc=host \ --restart always \
|
||||
--network bridge \
|
||||
-e HF_HUB_OFFLINE=1 \
|
||||
-e HF_HOME=/data/cache \
|
||||
-e TRANSFORMERS_OFFLINE=1 \
|
||||
--entrypoint python3 \
|
||||
vllm/vllm-openai:latest \
|
||||
-m vllm.entrypoints.openai.api_server \
|
||||
--model /data/Qwen3.5-27B-FP8 \
|
||||
--host 0.0.0.0 \
|
||||
--port 8000 \
|
||||
--gpu-memory-utilization 0.65 \
|
||||
--max-model-len 5120 \
|
||||
--max-num-seqs 8 \
|
||||
--enable-chunked-prefill \
|
||||
--max-num-batched-tokens 4096 \
|
||||
|
||||
=========== LLM ===========
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import math
|
||||
import os
|
||||
import torch
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.inputs.data import TokensPrompt
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
# 오프라인 환경 변수 강제 설정
|
||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# 1. 모델 경로 (컨테이너 내부 경로 기준)
|
||||
MODEL_PATH = "/data/qwen3-reranker-4b"
|
||||
|
||||
# 토크나이저 및 모델 로드
|
||||
# vllm-openai 이미지에는 이미 transformers, vllm, fastapi가 들어있습니다.
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, local_files_only=True)
|
||||
model = LLM(
|
||||
model=MODEL_PATH,
|
||||
gpu_memory_utilization=0.10, # H100 80GB 중 24GB 점유 (남은 공간은 80B 모델용)
|
||||
max_model_len=4096,
|
||||
max_num_seqs=20,
|
||||
trust_remote_code=True,
|
||||
dtype="float16",
|
||||
enforce_eager=True # 오프라인 환경에서 불필요한 커널 컴파일 방지
|
||||
)
|
||||
|
||||
# 토큰 설정
|
||||
true_token = tokenizer("yes", add_special_tokens=False).input_ids[0]
|
||||
false_token = tokenizer("no", add_special_tokens=False).input_ids[0]
|
||||
suffix = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
|
||||
suffix_tokens = tokenizer.encode(suffix, add_special_tokens=False)
|
||||
|
||||
class RerankRequest(BaseModel):
|
||||
query: str
|
||||
documents: List[str]
|
||||
|
||||
@app.post("/rerank")
|
||||
async def rerank(request: RerankRequest):
|
||||
task = 'Given a web search query, retrieve relevant passages that answer the query'
|
||||
prompts = []
|
||||
|
||||
for doc in request.documents:
|
||||
messages = [
|
||||
{"role": "system", "content": "Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\"."},
|
||||
{"role": "user", "content": f"<Instruct>: {task}\n\n<Query>: {request.query}\n\n<Document>: {doc}"}
|
||||
]
|
||||
token_ids = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=False)
|
||||
# 길이 제한 및 suffix 추가
|
||||
token_ids = token_ids[:8192 - len(suffix_tokens)] + suffix_tokens
|
||||
prompts.append(TokensPrompt(prompt_token_ids=token_ids))
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0, max_tokens=1, logprobs=20,
|
||||
allowed_token_ids=[true_token, false_token]
|
||||
)
|
||||
|
||||
outputs = model.generate(prompts, sampling_params, use_tqdm=False)
|
||||
|
||||
results = []
|
||||
for i, output in enumerate(outputs):
|
||||
final_logits = output.outputs[0].logprobs[-1]
|
||||
t_logit = final_logits[true_token].logprob if true_token in final_logits else -10.0
|
||||
f_logit = final_logits[false_token].logprob if false_token in final_logits else -10.0
|
||||
|
||||
t_score = math.exp(t_logit)
|
||||
f_score = math.exp(f_logit)
|
||||
score = t_score / (t_score + f_score)
|
||||
results.append({"index": i, "score": score})
|
||||
|
||||
return {"results": sorted(results, key=lambda x: x['score'], reverse=True)}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user