| |
| |
| import sys |
| from pathlib import Path |
|
|
| DIR = Path(__file__).resolve().parent.parent |
| sys.path.insert(0, str(DIR)) |
|
|
| import logging |
| import os |
| import json |
| from typing import Any, Dict, List, Optional |
| import torch |
|
|
| from models.medgemma import MedGemma |
| from models.config import parse_configs, load_config |
|
|
| logger = logging.getLogger(__name__) |
|
|
| class AttrDict(dict): |
| """同时支持 config.key 和 config['key']。""" |
|
|
| def __getattr__(self, key): |
| try: |
| return self[key] |
| except KeyError as exc: |
| raise AttributeError(key) from exc |
|
|
| def __setattr__(self, key, value): |
| self[key] = value |
|
|
| def to_attr_dict(value): |
| if isinstance(value, dict): |
| return AttrDict({ |
| key: to_attr_dict(item) |
| for key, item in value.items() |
| }) |
|
|
| if isinstance(value, list): |
| return [to_attr_dict(item) for item in value] |
|
|
| if isinstance(value, tuple): |
| return tuple(to_attr_dict(item) for item in value) |
|
|
| return value |
|
|
|
|
| class MedicalInferenceRunner: |
| """ |
| MedGemma 医学推理运行器 |
| 提供统一的推理接口 |
| """ |
|
|
| def __init__(self, configs: Any) -> None: |
| """ |
| 初始化推理运行器 |
| |
| Args: |
| configs: 配置对象 |
| """ |
| |
| self.configs = to_attr_dict(configs) |
| self.init_env() |
| self.init_basics() |
| self.init_model() |
| self.init_dumper() |
|
|
| logger.info("MedicalInferenceRunner initialized") |
|
|
| def init_env(self) -> None: |
| """初始化环境""" |
| self.use_cuda = torch.cuda.is_available() |
| if self.use_cuda: |
| self.device = torch.device("cuda:0") |
| torch.cuda.set_device(self.device) |
| logger.info(f"Using GPU: {torch.cuda.get_device_name(0)}") |
| else: |
| self.device = torch.device("cpu") |
| logger.info("Using CPU") |
|
|
| def init_basics(self) -> None: |
| """初始化基础设置""" |
| self.dump_dir = self.configs.output.dump_dir |
| self.error_dir = os.path.join(self.dump_dir, "errors") |
| os.makedirs(self.dump_dir, exist_ok=True) |
| os.makedirs(self.error_dir, exist_ok=True) |
| logger.info(f"Output directory: {self.dump_dir}") |
|
|
| def init_model(self) -> None: |
| """初始化 MedGemma 模型""" |
| logger.info("Loading MedGemma model...") |
| try: |
| self.model = MedGemma(self.configs) |
| logger.info("Model loaded successfully") |
| except Exception as e: |
| logger.error(f"Failed to load model: {e}") |
| raise |
|
|
| def init_dumper(self) -> None: |
| """初始化结果保存器""" |
| self.output_format = self.configs.output.output_format |
| self.save_predictions = self.configs.output.save_predictions |
| logger.info(f"Output format: {self.output_format}") |
|
|
| @torch.no_grad() |
| def predict( |
| self, |
| messages: List[Dict[str, Any]], |
| max_tokens: Optional[int] = None, |
| temperature: Optional[float] = None, |
| ) -> Dict[str, Any]: |
| """ |
| 运行推理 |
| |
| Args: |
| messages: 消息列表 |
| max_tokens: 最大生成 token 数 |
| temperature: 采样温度 |
| |
| Returns: |
| 预测结果 |
| """ |
| if max_tokens is None: |
| max_tokens = self.configs.inference.default_max_tokens |
| if temperature is None: |
| temperature = self.configs.inference.temperature |
|
|
| try: |
| result = self.model.forward( |
| messages=messages, |
| max_tokens=max_tokens, |
| temperature=temperature, |
| ) |
| return result |
| except Exception as e: |
| logger.error(f"Prediction failed: {e}") |
| return {"error": str(e)} |
|
|
| def run_from_file(self, input_path: str) -> None: |
| """ |
| 从文件运行推理 |
| |
| Args: |
| input_path: 输入文件路径(JSON 或 JSONL) |
| """ |
| logger.info(f"Loading input from: {input_path}") |
|
|
| |
| if input_path.endswith('.jsonl'): |
| samples = self._load_jsonl(input_path) |
| elif input_path.endswith('.json'): |
| samples = self._load_json(input_path) |
| else: |
| raise ValueError(f"Unsupported file format: {input_path}") |
|
|
| logger.info(f"Loaded {len(samples)} samples") |
|
|
| |
| results = [] |
| for idx, sample in enumerate(samples): |
| logger.info(f"Processing sample {idx + 1}/{len(samples)}") |
|
|
| try: |
| |
| if "messages" in sample: |
| messages = sample["messages"] |
| elif "text" in sample: |
| messages = [{"role": "user", "content": sample["text"]}] |
| elif "question" in sample: |
| messages = [{"role": "user", "content": sample["question"]}] |
| else: |
| logger.warning(f"Sample {idx} has no valid input") |
| continue |
|
|
| |
| result = self.predict(messages) |
|
|
| |
| result["sample_id"] = sample.get("id", idx) |
|
|
| |
| if self.save_predictions: |
| self._save_result(result, idx) |
|
|
| results.append(result) |
|
|
| except Exception as e: |
| logger.error(f"Error processing sample {idx}: {e}") |
| self._save_error(sample, idx, str(e)) |
|
|
| logger.info(f"Completed processing {len(results)} samples") |
|
|
| |
| self._save_summary(results) |
|
|
| def _load_json(self, filepath: str) -> List[Dict[str, Any]]: |
| """加载 JSON 文件""" |
| with open(filepath, 'r', encoding='utf-8') as f: |
| data = json.load(f) |
|
|
| if isinstance(data, list): |
| return data |
| else: |
| return [data] |
|
|
| def _load_jsonl(self, filepath: str) -> List[Dict[str, Any]]: |
| """加载 JSONL 文件""" |
| samples = [] |
| with open(filepath, 'r', encoding='utf-8') as f: |
| for line in f: |
| if line.strip(): |
| samples.append(json.loads(line)) |
| return samples |
|
|
| def _save_result(self, result: Dict[str, Any], idx: int) -> None: |
| """保存单个结果""" |
| output_path = os.path.join( |
| self.dump_dir, |
| f"prediction_{idx}.{self.output_format}" |
| ) |
|
|
| with open(output_path, 'w', encoding='utf-8') as f: |
| json.dump(result, f, indent=2, ensure_ascii=False) |
|
|
| def _save_error(self, sample: Dict[str, Any], idx: int, error: str) -> None: |
| """保存错误信息""" |
| error_path = os.path.join( |
| self.error_dir, |
| f"error_{idx}.json" |
| ) |
|
|
| error_data = { |
| "sample": sample, |
| "error": error, |
| } |
|
|
| with open(error_path, 'w', encoding='utf-8') as f: |
| json.dump(error_data, f, indent=2, ensure_ascii=False) |
|
|
| def _save_summary(self, results: List[Dict[str, Any]]) -> None: |
| """保存汇总结果""" |
| summary_path = os.path.join(self.dump_dir, "summary.json") |
|
|
| summary = { |
| "total_samples": len(results), |
| "successful": sum(1 for r in results if "error" not in r), |
| "failed": sum(1 for r in results if "error" in r), |
| "results": results, |
| } |
|
|
| with open(summary_path, 'w', encoding='utf-8') as f: |
| json.dump(summary, f, indent=2, ensure_ascii=False) |
|
|
| logger.info(f"Summary saved to: {summary_path}") |
|
|
| def run_interactive(self) -> None: |
| """交互式推理""" |
| logger.info("Starting interactive mode. Type 'quit' to exit.") |
|
|
| while True: |
| try: |
| user_input = input("\nUser: ") |
| if user_input.lower() in ['quit', 'exit', 'q']: |
| break |
|
|
| messages = [{"role": "user", "content": user_input}] |
| result = self.predict(messages) |
|
|
| |
| if "choices" in result and result["choices"]: |
| response = result["choices"][0]["message"]["content"] |
| print(f"\nAssistant: {response}") |
| else: |
| print(f"\nError: {result}") |
|
|
| except KeyboardInterrupt: |
| break |
| except Exception as e: |
| logger.error(f"Error: {e}") |
|
|
| logger.info("Exiting interactive mode") |
|
|
|
|
| def main(): |
| """主函数""" |
| import argparse |
|
|
| parser = argparse.ArgumentParser(description="MedGemma Medical Inference Runner") |
| parser.add_argument("--config", type=str, required=True, help="Config file path") |
| parser.add_argument("--input", type=str, help="Input file path (JSON/JSONL)") |
| parser.add_argument("--interactive", action="store_true", help="Interactive mode") |
| parser.add_argument("--model_path", type=str, help="Override model path") |
| parser.add_argument("--dump_dir", type=str, help="Override output directory") |
|
|
| args = parser.parse_args() |
|
|
| |
| configs = load_config(args.config) |
|
|
| |
| if args.model_path: |
| configs.model.model_path = args.model_path |
| if args.dump_dir: |
| configs.output.dump_dir = args.dump_dir |
|
|
| |
| runner = MedicalInferenceRunner(configs) |
|
|
| |
| if args.interactive: |
| runner.run_interactive() |
| elif args.input: |
| runner.run_from_file(args.input) |
| else: |
| logger.error("Either --input or --interactive must be specified") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|