Vendor LLMmap / llm-verify / llm-fingerprint-detector under bash/fingerprint/tools so the three fingerprint benchmarks run with only /data1/eval mounted (no /data1/xii dependency): - run.py DEFAULT_TOOLS_ROOT prefers builtin tools/, falls back to /data1/xii - exclude .git / node_modules / template backups - detector dist/ (pre-built) retained; node_modules not needed at runtime
169 lines
5.2 KiB
Python
169 lines
5.2 KiB
Python
#!/usr/bin/env python
|
||
"""Minimal OpenAI-compatible server to serve Qwen3-4B on CPU for fingerprinting.
|
||
|
||
Exposes what llm-fingerprint-detector needs: GET /v1/models, POST /v1/chat/completions.
|
||
Hidden thinking is disabled server-side (enable_thinking=False) so single-token
|
||
probes return visible text immediately and the detector uses the high-confidence
|
||
'none' reasoning strategy.
|
||
|
||
Run: python serve_qwen_cpu.py [--model /data1/models/Qwen3-4B] [--port 30002]
|
||
"""
|
||
import argparse
|
||
import os
|
||
import threading
|
||
|
||
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
||
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
||
|
||
import torch
|
||
import uvicorn
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.responses import JSONResponse
|
||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
|
||
MODEL_ID = "Qwen3-4B"
|
||
_lock = threading.Lock()
|
||
_model = None
|
||
_tokenizer = None
|
||
|
||
app = FastAPI(title="Qwen3-4B CPU server (OpenAI-compatible)")
|
||
|
||
|
||
def load(ModelDir):
|
||
global _model, _tokenizer
|
||
tok = AutoTokenizer.from_pretrained(ModelDir, trust_remote_code=True)
|
||
if tok.pad_token is None:
|
||
tok.pad_token = tok.eos_token
|
||
model = AutoModelForCausalLM.from_pretrained(
|
||
ModelDir,
|
||
torch_dtype=torch.bfloat16,
|
||
device_map="auto",
|
||
trust_remote_code=True,
|
||
)
|
||
model.eval()
|
||
_tokenizer, _model = tok, model
|
||
print(f"[server] loaded {ModelDir} on backend:", model.device)
|
||
|
||
|
||
def _generate(messages, temperature, max_tokens):
|
||
global _model, _tokenizer
|
||
tok, model = _tokenizer, _model
|
||
text = tok.apply_chat_template(
|
||
messages,
|
||
tokenize=False,
|
||
add_generation_prompt=True,
|
||
enable_thinking=False, # disable hidden thinking
|
||
)
|
||
in_toks = tok(text, return_tensors="pt", return_token_type_ids=False).to(model.device)
|
||
gen_kwargs = dict(
|
||
max_new_tokens=max(1, int(max_tokens or 16)),
|
||
pad_token_id=tok.eos_token_id,
|
||
eos_token_id=tok.eos_token_id,
|
||
)
|
||
temp = float(temperature) if temperature is not None else 1.0
|
||
if temp > 0:
|
||
gen_kwargs.update(do_sample=True, temperature=temp, top_p=0.95)
|
||
else:
|
||
gen_kwargs.update(do_sample=False)
|
||
with torch.no_grad():
|
||
out = model.generate(**in_toks, **gen_kwargs)
|
||
gen = out[0, in_toks.input_ids.shape[1]:]
|
||
return tok.decode(gen, skip_special_tokens=True)
|
||
|
||
|
||
@app.get("/health")
|
||
async def health():
|
||
return {"status": "ok"}
|
||
|
||
|
||
@app.get("/v1/models")
|
||
async def models():
|
||
return {
|
||
"object": "list",
|
||
"data": [{"id": MODEL_ID, "object": "model", "owned_by": "local-cpu"}],
|
||
}
|
||
|
||
|
||
@app.post("/v1/chat/completions")
|
||
async def chat(request: Request):
|
||
body = await request.json()
|
||
messages = [
|
||
{"role": m["role"], "content": m["content"]}
|
||
for m in body.get("messages", [])
|
||
if m.get("role") in ("system", "user", "assistant")
|
||
]
|
||
max_tokens = body.get("max_tokens", 16)
|
||
temperature = body.get("temperature", 1.0)
|
||
|
||
def run():
|
||
with _lock:
|
||
return _generate(messages, temperature, max_tokens)
|
||
|
||
import asyncio
|
||
loop = asyncio.get_event_loop()
|
||
content = await loop.run_in_executor(None, run)
|
||
model_name = body.get("model", MODEL_ID)
|
||
import time
|
||
base = {
|
||
"id": "chatcmpl-local",
|
||
"object": "chat.completion",
|
||
"created": int(time.time()),
|
||
"model": model_name,
|
||
"system_fingerprint": "qwen-cpu-local",
|
||
}
|
||
usage = {"prompt_tokens": 0, "completion_tokens": len(content),
|
||
"total_tokens": len(content)}
|
||
|
||
# 流式请求:按 OpenAI SSE 协议分块吐出(evalscope 默认 stream=True)
|
||
if body.get("stream"):
|
||
import json as _json
|
||
from fastapi.responses import StreamingResponse
|
||
|
||
def sse():
|
||
def chunk(delta, finish=None, usage=None):
|
||
payload = {**base, "object": "chat.completion.chunk",
|
||
"choices": [{"index": 0,
|
||
"delta": delta,
|
||
"finish_reason": finish}]}
|
||
if usage:
|
||
payload["usage"] = usage
|
||
return "data: " + _json.dumps(payload) + "\n\n"
|
||
yield chunk({"role": "assistant"})
|
||
step = max(1, len(content) // 8)
|
||
for i in range(0, len(content), step):
|
||
yield chunk({"content": content[i:i + step]})
|
||
yield chunk({}, "stop", usage=usage)
|
||
yield "data: [DONE]\n\n"
|
||
|
||
return StreamingResponse(sse(), media_type="text/event-stream")
|
||
|
||
return JSONResponse({
|
||
**base,
|
||
"choices": [{
|
||
"index": 0,
|
||
"message": {"role": "assistant", "content": content},
|
||
"finish_reason": "stop",
|
||
}],
|
||
"usage": usage,
|
||
})
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--model", default="/data1/models/Qwen3-4B")
|
||
ap.add_argument("--port", type=int, default=30002)
|
||
ap.add_argument("--host", default="0.0.0.0")
|
||
args = ap.parse_args()
|
||
|
||
import asyncio
|
||
load(args.model)
|
||
loop = asyncio.new_event_loop()
|
||
app.state.loop = loop
|
||
config = uvicorn.Config(app, host=args.host, port=args.port, log_level="info")
|
||
server = uvicorn.Server(config)
|
||
loop.run_until_complete(server.serve())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|