Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| 虫群本地推理后端 | |
| - 加载训练好的SwarmModel模型 | |
| - 提供统一的推理接口 | |
| - 支持多模型管理和切换 | |
| - 与MOA引擎集成 | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import json | |
| import torch | |
| import threading | |
| from typing import Dict, List, Optional, Tuple | |
| sys.path.insert(0, "/home/admin/swarm") | |
| from training.model import SwarmModel | |
| from training.tokenizer import SwarmTokenizer | |
| class LocalModelBackend: | |
| """ | |
| 本地模型推理后端 | |
| 管理多个本地小模型的加载、推理和卸载。 | |
| 按需加载模型到内存,支持LRU淘汰。 | |
| """ | |
| def __init__(self, models_dir: str = "/home/admin/swarm/training/models", max_loaded: int = 3): | |
| self.models_dir = models_dir | |
| self.max_loaded = max_loaded | |
| # 已加载的模型: model_id -> (model, tokenizer) | |
| self._loaded: Dict[str, Tuple[SwarmModel, SwarmTokenizer]] = {} | |
| self._load_times: Dict[str, float] = {} # LRU时间戳 | |
| self._lock = threading.Lock() | |
| # 可用模型列表 | |
| self._available = self._scan_models() | |
| # 设备 | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| def _scan_models(self) -> Dict[str, dict]: | |
| """扫描可用模型""" | |
| available = {} | |
| if not os.path.exists(self.models_dir): | |
| return available | |
| # 共享分词器路径 | |
| shared_tok = os.path.join(self.models_dir, "shared_tokenizer.json") | |
| # v2分词器 | |
| v2_tok = os.path.join(self.models_dir, "tokenizer_v2.json") | |
| for name in os.listdir(self.models_dir): | |
| model_dir = os.path.join(self.models_dir, name) | |
| if not os.path.isdir(model_dir): | |
| continue | |
| model_path = os.path.join(model_dir, "model.pt") | |
| meta_path = os.path.join(model_dir, "meta.json") | |
| if os.path.exists(model_path): | |
| meta = {} | |
| if os.path.exists(meta_path): | |
| with open(meta_path, "r") as f: | |
| meta = json.load(f) | |
| # 优先用模型目录下的分词器,否则用共享分词器或v2分词器 | |
| tok_path = os.path.join(model_dir, "tokenizer.json") | |
| if not os.path.exists(tok_path) and os.path.exists(shared_tok): | |
| tok_path = shared_tok | |
| if not os.path.exists(tok_path) and os.path.exists(v2_tok): | |
| tok_path = v2_tok | |
| # 检测量化模型 | |
| quantized = os.path.exists(os.path.join(model_dir, "quantized_int8")) | |
| available[name] = { | |
| "path": model_dir, | |
| "model_path": model_path, | |
| "tokenizer_path": tok_path, | |
| "meta": meta, | |
| "quantized": quantized, | |
| } | |
| return available | |
| def list_available(self) -> List[str]: | |
| """列出可用模型""" | |
| return list(self._available.keys()) | |
| def get_model_info(self, model_id: str) -> Optional[dict]: | |
| """获取模型信息""" | |
| if model_id in self._available: | |
| return self._available[model_id].get("meta", {}) | |
| if model_id in self._loaded: | |
| model, _ = self._loaded[model_id] | |
| return model.get_info() | |
| return None | |
| def load(self, model_id: str) -> bool: | |
| """加载模型到内存""" | |
| if model_id in self._loaded: | |
| self._load_times[model_id] = time.time() | |
| return True | |
| if model_id not in self._available: | |
| print(f" ❌ 模型不存在: {model_id}") | |
| return False | |
| # LRU淘汰 | |
| with self._lock: | |
| while len(self._loaded) >= self.max_loaded: | |
| oldest = min(self._load_times, key=self._load_times.get) | |
| self.unload(oldest) | |
| try: | |
| info = self._available[model_id] | |
| # 加载分词器 | |
| tokenizer = SwarmTokenizer.load(info["tokenizer_path"]) | |
| # 加载模型 | |
| checkpoint = torch.load(info["model_path"], map_location=self.device, weights_only=False) | |
| saved_cfg = checkpoint.get("config", {}) | |
| # 从权重推断实际max_len(保存的max_len可能不准) | |
| actual_max_len = 256 | |
| state_dict = checkpoint.get("model_state_dict", {}) | |
| if "pos_emb.weight" in state_dict: | |
| actual_max_len = state_dict["pos_emb.weight"].shape[0] | |
| elif "max_len" in checkpoint: | |
| actual_max_len = checkpoint["max_len"] | |
| model = SwarmModel( | |
| vocab_size=checkpoint.get("vocab_size", tokenizer.vocab_size_actual), | |
| d_model=saved_cfg.get("d_model", 192 if model_id == "tiny" else 320), | |
| n_heads=saved_cfg.get("n_heads", 6 if model_id == "tiny" else 8), | |
| n_layers=saved_cfg.get("n_layers", 6 if model_id == "tiny" else 8), | |
| d_ff=saved_cfg.get("d_model", 192) * 4, # 从d_model推算 | |
| max_len=actual_max_len, | |
| ) | |
| model.load_state_dict(checkpoint["model_state_dict"]) | |
| model = model.to(self.device) | |
| model.eval() | |
| with self._lock: | |
| self._loaded[model_id] = (model, tokenizer) | |
| self._load_times[model_id] = time.time() | |
| param_count = model.get_info()["params_M"] | |
| print(f" ✅ 模型已加载: {model_id} ({param_count}M参数)") | |
| return True | |
| except Exception as e: | |
| print(f" ❌ 加载失败: {model_id}, {e}") | |
| return False | |
| def unload(self, model_id: str) -> bool: | |
| """卸载模型""" | |
| with self._lock: | |
| if model_id in self._loaded: | |
| del self._loaded[model_id] | |
| del self._load_times[model_id] | |
| # 释放GPU内存 | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| print(f" 🗑️ 模型已卸载: {model_id}") | |
| return True | |
| return False | |
| def infer( | |
| self, | |
| model_id: str, | |
| query: str, | |
| max_new_tokens: int = 128, | |
| temperature: float = 0.7, | |
| top_k: int = 40, | |
| ) -> Dict: | |
| """ | |
| 推理接口 | |
| 返回: | |
| { | |
| "response": str, | |
| "model_id": str, | |
| "latency_ms": float, | |
| "tokens_generated": int, | |
| "success": bool, | |
| } | |
| """ | |
| # 确保模型已加载 | |
| if model_id not in self._loaded: | |
| if not self.load(model_id): | |
| return { | |
| "response": "", | |
| "model_id": model_id, | |
| "latency_ms": 0, | |
| "tokens_generated": 0, | |
| "success": False, | |
| } | |
| model, tokenizer = self._loaded[model_id] | |
| start_time = time.perf_counter() | |
| try: | |
| # 编码输入(只加BOS,不加EOS——EOS是生成终止标记) | |
| bos_id = tokenizer.bos_id | |
| input_ids = [bos_id] + tokenizer.encode(query, add_special=False) | |
| input_tensor = torch.tensor([input_ids], dtype=torch.long).to(self.device) | |
| # 生成(启用重复惩罚,小模型必备) | |
| with torch.no_grad(): | |
| output_ids = model.generate( | |
| input_tensor, | |
| max_new_tokens=max_new_tokens, | |
| temperature=temperature, | |
| top_k=top_k, | |
| eos_id=tokenizer.eos_id, | |
| repetition_penalty=1.3, | |
| ) | |
| # 解码 | |
| output_ids = output_ids[0].cpu().tolist() | |
| # 去掉输入部分 | |
| generated_ids = output_ids[len(input_ids):] | |
| response = tokenizer.decode(generated_ids) | |
| # 清理 | |
| response = response.replace("[UNK]", "").strip() | |
| latency_ms = (time.perf_counter() - start_time) * 1000 | |
| return { | |
| "response": response, | |
| "model_id": model_id, | |
| "latency_ms": round(latency_ms, 1), | |
| "tokens_generated": len(generated_ids), | |
| "success": True, | |
| } | |
| except Exception as e: | |
| latency_ms = (time.perf_counter() - start_time) * 1000 | |
| return { | |
| "response": f"推理错误: {str(e)[:100]}", | |
| "model_id": model_id, | |
| "latency_ms": round(latency_ms, 1), | |
| "tokens_generated": 0, | |
| "success": False, | |
| } | |
| def get_status(self) -> Dict: | |
| """后端状态""" | |
| return { | |
| "available_models": list(self._available.keys()), | |
| "loaded_models": list(self._loaded.keys()), | |
| "max_loaded": self.max_loaded, | |
| "device": str(self.device), | |
| } | |
| # 全局单例 | |
| _backend = None | |
| _backend_lock = threading.Lock() | |
| def get_local_backend() -> LocalModelBackend: | |
| """获取本地推理后端单例""" | |
| global _backend | |
| with _backend_lock: | |
| if _backend is None: | |
| _backend = LocalModelBackend() | |
| return _backend | |
| if __name__ == "__main__": | |
| # 测试 | |
| backend = LocalModelBackend() | |
| print(f"可用模型: {backend.list_available()}") | |
| print(f"状态: {backend.get_status()}") | |
| for model_id in backend.list_available(): | |
| print(f"\n--- 测试 {model_id} ---") | |
| result = backend.infer(model_id, "你好") | |
| print(f" 回复: {result['response'][:100]}") | |
| print(f" 延迟: {result['latency_ms']}ms") | |
| print(f" 成功: {result['success']}") | |