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:
Macbook
2026-07-21 22:57:30 +09:00
commit 4b86b2a660
344 changed files with 45787 additions and 0 deletions
@@ -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)