Instructions to use gxydResearch/lanlao with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use gxydResearch/lanlao with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="gxydResearch/lanlao")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("gxydResearch/lanlao", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download systemone_server.py from gxydResearch/lanlao: direct link, hf CLI and curl.
- Browser
- Download file 25.1 kB
-
https://huggingface.co/gxydResearch/lanlao/resolve/main/systemone_server.py
- Command line
-
hf download hf://gxydResearch/lanlao/systemone_server.py
-
curl -L -o systemone_server.py https://huggingface.co/gxydResearch/lanlao/resolve/main/systemone_server.py
25.1 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """Phase 10.1(RESEARCH_ROADMAP):/v1/systemone 本地兼容服务(Jev 客户端可无缝切换)。 | |
| 契约(对齐官方 typesafe_demo.py + jev_live_eval.py 实测行级输出): | |
| POST /v1/systemone {"state": str, "model": str, "questions": {name: {"type": "noul"|"choice"|"score", "instructions": str, "criteria": ...}}} | |
| - choice: criteria = {id: description}(无序) | |
| - score: criteria = [description, ...](由低到高有序等级) | |
| - noul: 无 criteria,instructions 即判据 | |
| 响应 {"answers": {name: answer}, "model", "usage": {"input_tokens", "output_tokens"}, "latency_ms"} | |
| - noul: {"noul": p_yes, "confidence": max(p_yes, 1-p_yes), "probabilities": {"yes": p, "no": 1-p}} | |
| - choice: {"choice": id, "confidence": maxp, "probabilities": {id: p}} | |
| - score: {"score": E[rank]/(k-1) ∈ [0,1], "confidence": maxp, "probabilities": {description: p}} | |
| 实现:frozen Qwen3.5-4B + 三种打分模式 | |
| --mode direct SemIf direct-logit(末位 logits 限 A-P slot → softmax,enable_thinking=False 硬前提) | |
| --mode lora LoRA checkpoint(scale_lora_train/rlcd_train 产物)merge 进基座后走 direct-logit;最终模型 rlcd_score43k_v5.pt(Phase 11 选型,authored 0.958/perturb 0.972) | |
| --mode head AttentionHead checkpoint(scale_head_train 产物)+ 冻结编码器(KV-Cache 广播是 10.2,本版逐问题前向) | |
| confidence 一律为分布汇总值(无第二头,对齐 Jev 黑盒形态)。 | |
| CLI(远程 4090):CUDA_VISIBLE_DEVICES=1 python systemone_server.py --model /home/chenzhimin/modelscope/models/Qwen--Qwen3.5-4B/snapshots/master --port 8100 --mode lora --lora-checkpoint /data20T/semif-results/rlcd_score43k_v5.pt | |
| 自测:python systemone_server.py --selftest ...(跑官方 demo 5 场景,不调外部 API) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import os | |
| import sys | |
| import time | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from pathlib import Path | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parent | |
| sys.path.insert(0, str(ROOT / "src")) | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from semif_phase1.core import LETTERS, DIRECT_SYSTEM # noqa: E402 | |
| from toy_dual_head import load_torch # noqa: E402 | |
| import torch # noqa: E402 | |
| from torch import nn # noqa: E402 | |
| import torch.nn.functional as F # noqa: E402 | |
| LETTER_SLOTS_CACHE: dict[str, int] = {} | |
| def softmax(values: list[float]) -> list[float]: | |
| peak = max(values) | |
| weights = [math.exp(v - peak) for v in values] | |
| total = sum(weights) | |
| return [w / total for w in weights] | |
| class DirectScorer: | |
| """SemIf direct-logit:末位 logits 限 A-P slot → softmax。score_batch 为 10.2 优化路径(共享前缀 KV-Cache 广播)。""" | |
| def __init__(self, model, tokenizer, device) -> None: | |
| self.model = model | |
| self.tokenizer = tokenizer | |
| self.device = device | |
| def build_prompt(self, state: str, instructions: str, option_descriptions: list[str]) -> str: | |
| payload = {"evidence": state, "criterion": instructions, "options": [{"letter": LETTERS[i], "description": description} for i, description in enumerate(option_descriptions)]} | |
| return self.tokenizer.apply_chat_template([{"role": "system", "content": DIRECT_SYSTEM}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}], tokenize=False, add_generation_prompt=True, enable_thinking=False) | |
| def score(self, state: str, instructions: str, option_descriptions: list[str]) -> list[float]: | |
| prompt = self.build_prompt(state, instructions, option_descriptions) | |
| ids = self.tokenizer.encode(prompt, add_special_tokens=False) | |
| with torch.inference_mode(): | |
| inp = torch.tensor([ids], dtype=torch.long, device=self.device) | |
| logits = self.model(input_ids=inp).logits[0, -1].float().cpu() | |
| return self._slot_softmax(logits, option_descriptions) | |
| def _slot_softmax(self, logits: torch.Tensor, option_descriptions: list[str]) -> list[float]: | |
| slots = [] | |
| for i in range(len(option_descriptions)): | |
| letter = LETTERS[i] | |
| if letter not in LETTER_SLOTS_CACHE: | |
| token_ids = self.tokenizer.encode(letter, add_special_tokens=False) | |
| if len(token_ids) != 1: | |
| raise ValueError(f"slot {letter} not single token: {token_ids}") | |
| LETTER_SLOTS_CACHE[letter] = token_ids[0] | |
| slots.append(LETTER_SLOTS_CACHE[letter]) | |
| selected = [float(logits[slot]) for slot in slots] | |
| return softmax(selected) | |
| def score_batch(self, state: str, questions: list[tuple[str, list[str]]]) -> list[list[float]]: | |
| """10.2:同一 state 的多问题共享前缀——prefix 编码一次(线性态缓存),每问题恢复快照再前向后缀。失败自动回退逐条全量前向。""" | |
| if len(questions) <= 1 or not self._cache_supported(): | |
| return [self.score(state, instructions, option_descriptions) for instructions, option_descriptions in questions] | |
| prompts = [self.build_prompt(state, instructions, option_descriptions) for instructions, option_descriptions in questions] | |
| encoded = [self.tokenizer.encode(prompt, add_special_tokens=False) for prompt in prompts] | |
| common = 0 | |
| limit = min(len(ids) for ids in encoded) | |
| while common < limit and all(ids[common] == encoded[0][common] for ids in encoded): | |
| common += 1 | |
| if common < 32: | |
| return [self.score(state, instructions, option_descriptions) for instructions, option_descriptions in questions] | |
| try: | |
| results = [] | |
| prefix_ids = encoded[0][:common] | |
| with torch.inference_mode(): | |
| prefix_tensor = torch.tensor([prefix_ids], dtype=torch.long, device=self.device) | |
| cache = self._make_cache() | |
| self.model(input_ids=prefix_tensor, past_key_values=cache, use_cache=True) | |
| snapshot = self._snapshot(cache) | |
| for index, ids in enumerate(encoded): | |
| self._restore(cache, snapshot) | |
| suffix = torch.tensor([ids[common:]], dtype=torch.long, device=self.device) | |
| logits = self.model(input_ids=suffix, past_key_values=cache, use_cache=True).logits[0, -1].float().cpu() | |
| option_descriptions = questions[index][1] | |
| results.append(self._slot_softmax(logits, option_descriptions)) | |
| return results | |
| except Exception as error: | |
| print(f"[score_batch] cache path failed ({type(error).__name__}: {error}), falling back to full forward", flush=True) | |
| self._cache_ok = False | |
| return [self.score(state, instructions, option_descriptions) for instructions, option_descriptions in questions] | |
| def _cache_supported(self) -> bool: | |
| return bool(getattr(self, "_cache_ok", True)) | |
| def _make_cache(self): | |
| from transformers import DynamicCache | |
| config = getattr(self.model, "config", None) | |
| if config is not None: | |
| try: | |
| return DynamicCache(config=config) | |
| except Exception: | |
| pass | |
| return DynamicCache() | |
| def _snapshot(self, cache) -> list: | |
| snapshot = [] | |
| names = ("conv_states", "recurrent_states", "key_states", "value_states") | |
| for layer in cache.layers: | |
| snapshot.append(tuple((name, self._clone_state(getattr(layer, name, None))) for name in names)) | |
| return snapshot | |
| def _restore(self, cache, snapshot: list) -> None: | |
| for layer, entries in zip(cache.layers, snapshot): | |
| for name, state in entries: | |
| if state is not None: | |
| setattr(layer, name, self._clone_state(state)) | |
| def _clone_state(self, state): | |
| if state is None: | |
| return None | |
| if isinstance(state, torch.Tensor): | |
| return state.detach().clone() | |
| if isinstance(state, dict): | |
| return {key: self._clone_state(value) for key, value in state.items()} | |
| if isinstance(state, (list, tuple)): | |
| cloned = [self._clone_state(item) for item in state] | |
| return tuple(cloned) if isinstance(state, tuple) else cloned | |
| return state | |
| class HeadScorer: | |
| """AttentionHead(scale_head_train checkpoint)+ 冻结编码器。context = chat prompt 全 token hidden,option = 描述 mean-pool。""" | |
| def __init__(self, model, tokenizer, device, checkpoint: Path, rank: int = 64, context_tokens: int = 640, option_tokens: int = 64) -> None: | |
| from scale_head_train import AttentionHead # noqa: F401 — 同步实现复用 | |
| self.model = model | |
| self.tokenizer = tokenizer | |
| self.device = device | |
| self.context_tokens = context_tokens | |
| self.option_tokens = option_tokens | |
| payload = torch.load(checkpoint, map_location="cpu", weights_only=False) | |
| config = payload.get("config", {}) | |
| self.head = AttentionHead(self.resolve_hidden(), int(config.get("head_rank", rank))).to(device) | |
| self.head.load_state_dict(payload["state_dict"]) | |
| self.head.eval() | |
| def resolve_hidden(self) -> int: | |
| hidden = getattr(self.model.config, "hidden_size", None) | |
| if hidden is None and hasattr(self.model.config, "get_text_config"): | |
| hidden = self.model.config.get_text_config().hidden_size | |
| if hidden is None: | |
| raise ValueError(f"cannot resolve hidden_size from config {type(self.model.config)}") | |
| return int(hidden) | |
| def encode_context(self, prompt: str) -> tuple[torch.Tensor, torch.Tensor]: | |
| ids = self.tokenizer.encode(prompt, add_special_tokens=False)[-self.context_tokens:] | |
| mask = torch.ones((1, len(ids)), dtype=torch.bool, device=self.device) | |
| with torch.inference_mode(): | |
| inp = torch.tensor([ids], dtype=torch.long, device=self.device) | |
| hidden = self.model.model(input_ids=inp).last_hidden_state.to(torch.float16) | |
| return hidden, mask | |
| def encode_options(self, descriptions: list[str]) -> torch.Tensor: | |
| vectors = [] | |
| with torch.inference_mode(): | |
| for description in descriptions: | |
| ids = self.tokenizer.encode(description, add_special_tokens=True)[: self.option_tokens] | |
| if not ids: | |
| raise ValueError("empty option tokens") | |
| inp = torch.tensor([ids], dtype=torch.long, device=self.device) | |
| hidden = self.model.model(input_ids=inp).last_hidden_state.float() | |
| vectors.append(hidden[0].mean(dim=0)) | |
| return torch.stack(vectors).to(torch.float16).unsqueeze(0) | |
| def score(self, state: str, instructions: str, option_descriptions: list[str]) -> list[float]: | |
| payload = {"evidence": state, "criterion": instructions, "options": [{"letter": LETTERS[i], "description": description} for i, description in enumerate(option_descriptions)]} | |
| prompt = self.tokenizer.apply_chat_template([{"role": "system", "content": DIRECT_SYSTEM}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}], tokenize=False, add_generation_prompt=True, enable_thinking=False) | |
| context, context_mask = self.encode_context(prompt) | |
| options = self.encode_options(option_descriptions) | |
| option_mask = torch.zeros((1, options.shape[1]), dtype=torch.bool, device=self.device) | |
| option_mask[0, :] = True | |
| with torch.inference_mode(): | |
| logits = self.head(context, context_mask, options, option_mask)[0] | |
| probs = F.softmax(logits, dim=-1).cpu().numpy().tolist() | |
| return probs | |
| NOUL_YES = "The criterion holds" | |
| NOUL_NO = "The criterion does not hold" | |
| def question_spec(question: dict) -> tuple[str, list[str]]: | |
| """把 Jev 三类型问题归一为 (instructions, option_descriptions)。""" | |
| question_type = question.get("type") | |
| instructions = str(question.get("instructions", "")).strip() | |
| if not instructions: | |
| raise ValueError("missing instructions") | |
| if question_type == "noul": | |
| return instructions, [NOUL_YES, NOUL_NO] | |
| if question_type == "choice": | |
| criteria = question.get("criteria") | |
| if not isinstance(criteria, dict) or not criteria: | |
| raise ValueError("choice requires non-empty criteria dict") | |
| return instructions, [str(value) for value in criteria.values()] | |
| if question_type == "score": | |
| criteria = question.get("criteria") | |
| if not isinstance(criteria, list) or len(criteria) < 2: | |
| raise ValueError("score requires ordered criteria list (>=2 levels)") | |
| return instructions, [str(item) for item in criteria] | |
| raise ValueError(f"unknown type {question_type}") | |
| def answer_request(scorer, state: str, questions: dict) -> dict: | |
| """请求级入口:同 state 多问题走 score_batch(10.2 共享前缀 KV-Cache 广播)。""" | |
| names = list(questions.keys()) | |
| specs = [question_spec(questions[name]) for name in names] | |
| if hasattr(scorer, "score_batch"): | |
| prob_lists = scorer.score_batch(state, specs) | |
| else: | |
| prob_lists = [scorer.score(state, instructions, option_descriptions) for instructions, option_descriptions in specs] | |
| answers = {} | |
| for name, question, (instructions, _), probs in zip(names, [questions[n] for n in names], specs, prob_lists): | |
| answers[name] = format_answer(question, probs) | |
| return answers | |
| def format_answer(question: dict, probs: list[float]) -> dict: | |
| question_type = question.get("type") | |
| if question_type == "noul": | |
| p_yes = float(probs[0]) | |
| return {"noul": p_yes, "confidence": max(p_yes, 1.0 - p_yes), "probabilities": {"yes": p_yes, "no": 1.0 - p_yes}} | |
| if question_type == "choice": | |
| criteria = question["criteria"] | |
| ids = list(criteria.keys()) | |
| best = int(np.argmax(probs)) | |
| return {"choice": ids[best], "confidence": float(probs[best]), "probabilities": {key: float(p) for key, p in zip(ids, probs)}} | |
| if question_type == "score": | |
| criteria = question["criteria"] | |
| k = len(criteria) | |
| expectation = sum(p * (i / (k - 1)) for i, p in enumerate(probs)) | |
| return {"score": float(expectation), "confidence": float(max(probs)), "probabilities": {str(item): float(p) for item, p in zip(criteria, probs)}} | |
| raise ValueError(f"unknown type {question_type}") | |
| def answer_question(scorer, state: str, name: str, question: dict) -> dict: | |
| question_type = question.get("type") | |
| instructions = str(question.get("instructions", "")).strip() | |
| if not instructions: | |
| raise ValueError(f"question {name} missing instructions") | |
| if question_type == "noul": | |
| probs = scorer.score(state, instructions, [NOUL_YES, NOUL_NO]) | |
| p_yes = float(probs[0]) | |
| return {"noul": p_yes, "confidence": max(p_yes, 1.0 - p_yes), "probabilities": {"yes": p_yes, "no": 1.0 - p_yes}} | |
| if question_type == "choice": | |
| criteria = question.get("criteria") | |
| if not isinstance(criteria, dict) or not criteria: | |
| raise ValueError(f"question {name} choice requires non-empty criteria dict") | |
| ids = list(criteria.keys()) | |
| descriptions = [str(criteria[key]) for key in ids] | |
| probs = scorer.score(state, instructions, descriptions) | |
| best = int(np.argmax(probs)) | |
| return {"choice": ids[best], "confidence": float(probs[best]), "probabilities": {key: float(p) for key, p in zip(ids, probs)}} | |
| if question_type == "score": | |
| criteria = question.get("criteria") | |
| if not isinstance(criteria, list) or len(criteria) < 2: | |
| raise ValueError(f"question {name} score requires ordered criteria list (>=2 levels)") | |
| descriptions = [str(item) for item in criteria] | |
| probs = scorer.score(state, instructions, descriptions) | |
| k = len(descriptions) | |
| expectation = sum(p * (i / (k - 1)) for i, p in enumerate(probs)) | |
| return {"score": float(expectation), "confidence": float(max(probs)), "probabilities": {description: float(p) for description, p in zip(descriptions, probs)}} | |
| raise ValueError(f"question {name} unknown type {question_type}") | |
| SELFTEST_SCENARIOS = [ | |
| {"name": "support ticket routing", "state": "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.", "questions": {"is_urgent": {"type": "noul", "instructions": "The message conveys urgency or time-sensitivity"}, "department": {"type": "choice", "instructions": "Which team should handle this", "criteria": {"billing": "Payment or subscription issues", "technical": "Bugs or integration problems", "sales": "Pricing or account questions"}}, "frustration": {"type": "score", "instructions": "How frustrated the customer appears", "criteria": ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"]}}}, | |
| {"name": "financial news risk", "state": "The Federal Reserve signaled it will hold interest rates steady this quarter, while inflation remains above target. Tech stocks fell 2% in pre-market trading.", "questions": {"is_bearish": {"type": "noul", "instructions": "The headline is overall negative for equity markets"}, "asset_class": {"type": "choice", "instructions": "Which asset class is most directly affected", "criteria": {"equities": "Stocks", "rates": "Bonds / interest rates", "crypto": "Cryptocurrencies", "fx": "Currencies"}}, "impact": {"type": "score", "instructions": "How large is the expected market impact", "criteria": ["Negligible", "Moderate", "Major"]}}}, | |
| {"name": "phishing email", "state": "Dear Customer, your account will be suspended in 24 hours unless you verify your password immediately by clicking this link: http://secure-login.xyz/verify", "questions": {"is_spam": {"type": "noul", "instructions": "This is unsolicited bulk or promotional email"}, "is_phishing": {"type": "noul", "instructions": "This email tries to trick the user into revealing credentials or clicking a malicious link"}, "risk": {"type": "choice", "instructions": "The primary risk category", "criteria": {"credential_theft": "Asks for login credentials or passwords", "malware": "Encourages downloading or running something", "scam": "Asks for money or personal data", "benign": "No clear malicious intent"}}}}, | |
| {"name": "code review", "state": "def divide(a, b):\n return a / b\n\n# used as: divide(10, 0)", "questions": {"has_bug": {"type": "noul", "instructions": "The code has a correctness bug that can crash or produce wrong results"}, "bug_type": {"type": "choice", "instructions": "The main defect, if any", "criteria": {"division_by_zero": "Does not guard against divide-by-zero", "type_error": "Wrong types or misuse", "logic_error": "Wrong logic / off-by-one", "none": "No clear defect"}}, "readability": {"type": "score", "instructions": "How readable and well-structured the code is", "criteria": ["Poor", "Okay", "Clean"]}}}, | |
| {"name": "review scoring", "state": "The laptop is fast and the screen is gorgeous, but the battery only lasts 3 hours and the price is way over what I expected. Would buy it again for the performance.", "questions": {"performance": {"type": "score", "instructions": "How well performance is reviewed", "criteria": ["Poor", "Average", "Excellent"]}, "battery": {"type": "score", "instructions": "How well battery life is reviewed", "criteria": ["Poor", "Average", "Excellent"]}, "value_for_money": {"type": "score", "instructions": "How well price/value is reviewed", "criteria": ["Poor", "Average", "Excellent"]}, "would_recommend": {"type": "noul", "instructions": "The reviewer would recommend this product"}}}, | |
| ] | |
| def run_selftest(scorer, tokenizer) -> None: | |
| for scenario in SELFTEST_SCENARIOS: | |
| started = time.time() | |
| answers = answer_request(scorer, scenario["state"], scenario["questions"]) | |
| latency = round((time.time() - started) * 1000, 1) | |
| print("=" * 70) | |
| print(f"【{scenario['name']}】latency={latency}ms") | |
| print(json.dumps(answers, ensure_ascii=False, indent=2)) | |
| class Handler(BaseHTTPRequestHandler): | |
| scorer = None | |
| tokenizer = None | |
| model_name = "semif-systemone" | |
| def do_POST(self): | |
| if self.path != "/v1/systemone": | |
| self.send_json(404, {"error": f"unknown path {self.path}"}) | |
| return | |
| try: | |
| length = int(self.headers.get("Content-Length", "0")) | |
| body = json.loads(self.rfile.read(length).decode("utf-8")) | |
| state = str(body.get("state", "")) | |
| questions = body.get("questions") | |
| if not state or not isinstance(questions, dict) or not questions: | |
| raise ValueError("body requires state and questions") | |
| started = time.time() | |
| answers = answer_request(self.scorer, state, questions) | |
| latency = round((time.time() - started) * 1000, 1) | |
| input_tokens = sum(len(self.tokenizer.encode(json.dumps({"state": state, "question": question}, ensure_ascii=False), add_special_tokens=False)) for question in questions.values()) | |
| self.send_json(200, {"answers": answers, "model": self.model_name, "usage": {"input_tokens": input_tokens, "output_tokens": 0}, "latency_ms": latency}) | |
| except Exception as error: | |
| self.send_json(400, {"error": f"{type(error).__name__}: {error}"}) | |
| def send_json(self, status: int, payload: dict) -> None: | |
| data = json.dumps(payload, ensure_ascii=False).encode("utf-8") | |
| self.send_response(status) | |
| self.send_header("Content-Type", "application/json") | |
| self.send_header("Content-Length", str(len(data))) | |
| self.end_headers() | |
| self.wfile.write(data) | |
| def log_message(self, format, *args): | |
| print(f"[{time.strftime('%H:%M:%S')}] {args[0]}", flush=True) | |
| def load_lora_checkpoint(model, checkpoint: Path): | |
| """把 scale_lora_train 保存的 lora_state merge 进基座(merge_and_unload 后即普通模型)。""" | |
| from peft import LoraConfig, get_peft_model | |
| payload = torch.load(checkpoint, map_location="cpu", weights_only=False) | |
| lora_state = payload["lora_state"] | |
| config = payload.get("config", {}) | |
| rank = int(config.get("rank", 32)) | |
| lora_config = LoraConfig(r=rank, lora_alpha=rank * 2, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.0, bias="none", task_type="CAUSAL_LM") | |
| wrapped = get_peft_model(model, lora_config) | |
| wrapped.load_state_dict(lora_state, strict=False) | |
| current = wrapped.state_dict() | |
| matched = sum(1 for name, tensor in lora_state.items() if name in current and current[name].shape == tensor.shape) | |
| print(f"lora checkpoint {checkpoint}: {len(lora_state)} tensors, matched={matched}", flush=True) | |
| if matched < len(lora_state) * 0.9: | |
| raise ValueError(f"lora state mismatch: matched {matched}/{len(lora_state)}") | |
| wrapped.eval() | |
| return wrapped.merge_and_unload() | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--model", type=str, default=os.path.expanduser("~/modelscope/models/Qwen--Qwen3.5-4B/snapshots/master")) | |
| parser.add_argument("--port", type=int, default=8100) | |
| parser.add_argument("--mode", choices=("direct", "lora", "head"), default="direct") | |
| parser.add_argument("--lora-checkpoint", type=Path, default=None) | |
| parser.add_argument("--head-checkpoint", type=Path, default=None) | |
| parser.add_argument("--selftest", action="store_true") | |
| args = parser.parse_args() | |
| device = torch.device("cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")) | |
| print(f"loading {args.model} on {device} (mode={args.mode})", flush=True) | |
| model, tokenizer, _ = load_torch(args.model) | |
| if device.type == "cuda": | |
| model = model.to("cuda:0") | |
| if args.mode == "lora": | |
| if args.lora_checkpoint is None: | |
| parser.error("--mode lora requires --lora-checkpoint (scale_lora_train 产物)") | |
| model = load_lora_checkpoint(model, args.lora_checkpoint) | |
| scorer = DirectScorer(model, tokenizer, device) | |
| Handler.model_name = "semif-systemone-lora" | |
| elif args.mode == "head": | |
| if args.head_checkpoint is None: | |
| parser.error("--mode head requires --head-checkpoint (scale_head_train 产物)") | |
| scorer = HeadScorer(model, tokenizer, device, args.head_checkpoint) | |
| Handler.model_name = "semif-systemone-head" | |
| else: | |
| scorer = DirectScorer(model, tokenizer, device) | |
| Handler.scorer = scorer | |
| Handler.tokenizer = tokenizer | |
| if args.selftest: | |
| run_selftest(scorer, tokenizer) | |
| return | |
| server = ThreadingHTTPServer(("0.0.0.0", args.port), Handler) | |
| print(f"listening on 0.0.0.0:{args.port} (POST /v1/systemone)", flush=True) | |
| server.serve_forever() | |
| if __name__ == "__main__": | |
| main() | |