Download inference/generating.py from hulehule/pllm2-full-dump: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/hulehule/pllm2-full-dump/resolve/main/inference/generating.py
- Command line
-
hf download hf://hulehule/pllm2-full-dump/inference/generating.py
-
curl -L -o generating.py https://huggingface.co/hulehule/pllm2-full-dump/resolve/main/inference/generating.py
24.4 kB
| ''' | |
| 多卡GPU inference: | |
| # 使用所有可用GPU | |
| python generating.py \ | |
| --test_data_path /opt/tiger/PLLM1.0/dataset/processed_data/abstract_test.json \ | |
| --output_path /opt/tiger/PLLM1.0/dataset/generated_answers.json \ | |
| --base_model meta-llama/Llama-2-7b-hf \ | |
| --embedding_model all-mpnet-base-v2 \ | |
| --cluster_info_path /opt/tiger/PLLM1.0/clustering/cluster_loras/cluster_info.json \ | |
| --lora_base_path /opt/tiger/PLLM1.0/clustering/cluster_loras \ | |
| --max_length 256 \ | |
| --temperature 0.3 | |
| # 指定使用4张GPU | |
| python multi_gpu_generation.py \ | |
| --num_gpus 4 \ | |
| # ... 其他参数相同 | |
| ''' | |
| import json | |
| import torch | |
| import torch.multiprocessing as mp | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from peft import PeftModel | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| from tqdm import tqdm | |
| import os | |
| from typing import List, Dict, Any, Tuple | |
| import argparse | |
| import gc | |
| import time | |
| import threading | |
| import queue | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| import fcntl | |
| import copy | |
| class MultiGPUAnswerGenerator: | |
| def __init__(self, | |
| base_model_name: str = "meta-llama/Llama-2-7b-hf", | |
| embedding_model: str = "all-mpnet-base-v2", | |
| cluster_info_path: str = "./cluster_loras/cluster_info.json", | |
| lora_base_path: str = "./cluster_loras", | |
| num_gpus: int = None): | |
| self.base_model_name = base_model_name | |
| self.embedding_model_name = embedding_model | |
| self.cluster_info_path = cluster_info_path | |
| self.lora_base_path = lora_base_path | |
| # 检测可用GPU数量 | |
| if num_gpus is None: | |
| self.num_gpus = torch.cuda.device_count() | |
| else: | |
| self.num_gpus = min(num_gpus, torch.cuda.device_count()) | |
| if self.num_gpus == 0: | |
| raise RuntimeError("没有检测到可用的GPU") | |
| print(f"🚀 使用 {self.num_gpus} 张GPU进行并行生成") | |
| for i in range(self.num_gpus): | |
| props = torch.cuda.get_device_properties(i) | |
| print(f" GPU {i}: {props.name} ({props.total_memory/1024**3:.1f}GB)") | |
| # 加载embedding模型 | |
| print(f"Loading embedding model: {embedding_model}") | |
| self.embedding_model = SentenceTransformer(embedding_model) | |
| # 加载聚类信息 | |
| self.cluster_info = self.load_cluster_info() | |
| self.cluster_centers = self.compute_cluster_centers() | |
| # 实时写入相关 | |
| self.write_lock = threading.Lock() | |
| self.results_queue = queue.Queue() | |
| self.writer_thread = None | |
| self.stop_writing = threading.Event() | |
| def load_cluster_info(self) -> Dict[str, Any]: | |
| """加载聚类信息""" | |
| print(f"Loading cluster info from {self.cluster_info_path}") | |
| with open(self.cluster_info_path, 'r', encoding='utf-8') as f: | |
| cluster_info = json.load(f) | |
| print(f"Loaded {cluster_info['n_clusters']} clusters") | |
| return cluster_info | |
| def compute_cluster_centers(self) -> np.ndarray: | |
| """计算聚类中心""" | |
| if 'cluster_centers' in self.cluster_info: | |
| centers = np.array(self.cluster_info['cluster_centers']) | |
| print(f"Using pre-computed cluster centers: {centers.shape}") | |
| return centers | |
| else: | |
| print("⚠️ No pre-computed cluster centers found") | |
| return None | |
| def extract_user_profile(self, user_data: Dict[str, Any]) -> str: | |
| """从用户数据中提取profile文本""" | |
| profile_text = "" | |
| if 'profile' in user_data and isinstance(user_data['profile'], list): | |
| profile_texts = [] | |
| for profile_item in user_data['profile']: | |
| if isinstance(profile_item, dict): | |
| if 'abstract' in profile_item: | |
| profile_texts.append(profile_item['abstract']) | |
| if 'title' in profile_item: | |
| profile_texts.append(profile_item['title']) | |
| profile_text = ' '.join(profile_texts) | |
| return profile_text | |
| def find_nearest_cluster(self, user_embedding: np.ndarray) -> int: | |
| """找到最近的聚类""" | |
| if self.cluster_centers is None: | |
| return 0 | |
| similarities = cosine_similarity([user_embedding], self.cluster_centers)[0] | |
| return np.argmax(similarities) | |
| def start_real_time_writer(self, output_path: str): | |
| """启动实时写入线程""" | |
| def writer_worker(): | |
| written_results = [] | |
| # 如果文件已存在,读取已有结果 | |
| if os.path.exists(output_path): | |
| try: | |
| with open(output_path, 'r', encoding='utf-8') as f: | |
| written_results = json.load(f) | |
| print(f"📂 恢复已有结果: {len(written_results)} 条记录") | |
| except: | |
| written_results = [] | |
| while not self.stop_writing.is_set() or not self.results_queue.empty(): | |
| try: | |
| # 等待新结果,超时1秒 | |
| new_results = self.results_queue.get(timeout=1.0) | |
| written_results.extend(new_results) | |
| # 实时写入文件 | |
| with self.write_lock: | |
| temp_path = output_path + '.tmp' | |
| with open(temp_path, 'w', encoding='utf-8') as f: | |
| json.dump(written_results, f, indent=2, ensure_ascii=False) | |
| # 原子性替换文件 | |
| os.replace(temp_path, output_path) | |
| print(f"💾 实时保存: 已写入 {len(written_results)} 条结果") | |
| except queue.Empty: | |
| continue | |
| except Exception as e: | |
| print(f"❌ 写入错误: {e}") | |
| self.writer_thread = threading.Thread(target=writer_worker, daemon=True) | |
| self.writer_thread.start() | |
| print("✅ 实时写入线程已启动") | |
| def stop_real_time_writer(self): | |
| """停止实时写入线程""" | |
| if self.writer_thread: | |
| self.stop_writing.set() | |
| self.writer_thread.join(timeout=10) | |
| print("✅ 实时写入线程已停止") | |
| def load_model_on_gpu(self, cluster_id: int, gpu_id: int) -> Tuple[Any, Any]: | |
| """在指定GPU上加载模型""" | |
| cluster_dir = os.path.join(self.lora_base_path, f"cluster_{cluster_id}") | |
| if not os.path.exists(cluster_dir): | |
| print(f"⚠️ Cluster {cluster_id} directory not found") | |
| return None, None | |
| try: | |
| print(f"🔧 GPU {gpu_id}: 加载 cluster_{cluster_id} 模型...") | |
| # 设置当前GPU | |
| torch.cuda.set_device(gpu_id) | |
| # 加载tokenizer | |
| tokenizer = AutoTokenizer.from_pretrained(cluster_dir) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| tokenizer.padding_side = "left" | |
| # 加载基础模型到指定GPU | |
| base_model = AutoModelForCausalLM.from_pretrained( | |
| self.base_model_name, | |
| torch_dtype=torch.float16, # 使用float16节省内存 | |
| device_map={f"": f"cuda:{gpu_id}"}, # 强制指定GPU | |
| low_cpu_mem_usage=True, | |
| max_memory={gpu_id: "70GB"} # 为H100预留一些内存 | |
| ) | |
| # 加载LoRA适配器 | |
| model = PeftModel.from_pretrained(base_model, cluster_dir) | |
| # 合并LoRA权重以提高推理速度 | |
| try: | |
| model = model.merge_and_unload() | |
| print(f"✅ GPU {gpu_id}: LoRA权重已合并") | |
| except Exception as e: | |
| print(f"⚠️ GPU {gpu_id}: LoRA权重合并失败: {e}") | |
| model.eval() | |
| print(f"✅ GPU {gpu_id}: cluster_{cluster_id} 模型加载完成") | |
| return model, tokenizer | |
| except Exception as e: | |
| print(f"❌ GPU {gpu_id}: 加载模型失败: {e}") | |
| return None, None | |
| def generate_batch_on_gpu(self, samples: List[Dict], cluster_id: int, gpu_id: int, | |
| max_length: int = 256, temperature: float = 0.3) -> List[Dict]: | |
| """在指定GPU上批量生成答案""" | |
| try: | |
| # 加载模型 | |
| model, tokenizer = self.load_model_on_gpu(cluster_id, gpu_id) | |
| if model is None or tokenizer is None: | |
| return self._create_empty_results(samples, cluster_id) | |
| results = [] | |
| batch_size = 4 # 每个GPU的batch size | |
| # 分批处理样本 | |
| for i in range(0, len(samples), batch_size): | |
| batch = samples[i:i+batch_size] | |
| batch_queries = [s['query'] for s in batch] | |
| try: | |
| # 编码输入 | |
| inputs = tokenizer( | |
| batch_queries, | |
| return_tensors="pt", | |
| padding=True, | |
| truncation=True, | |
| max_length=128 | |
| ) | |
| # 移动到指定GPU | |
| device = f"cuda:{gpu_id}" | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| # 生成答案 | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_new_tokens=max_length, | |
| temperature=temperature, | |
| do_sample=True, | |
| top_p=0.9, | |
| top_k=50, | |
| pad_token_id=tokenizer.eos_token_id, | |
| eos_token_id=tokenizer.eos_token_id, | |
| use_cache=True, | |
| repetition_penalty=1.1 | |
| ) | |
| # 解码答案 | |
| generated_texts = tokenizer.batch_decode(outputs, skip_special_tokens=True) | |
| # 构建结果 | |
| for j, sample in enumerate(batch): | |
| answer = generated_texts[j][len(batch_queries[j]):].strip() | |
| results.append({ | |
| 'user_id': sample['user_id'], | |
| 'query': sample['query'], | |
| 'profile_text': sample['profile_text'], | |
| 'nearest_cluster': cluster_id, | |
| 'generated_answer': answer, | |
| 'ground_truth': sample['ground_truth'], | |
| 'gpu_id': gpu_id # 记录使用的GPU | |
| }) | |
| # 每隔几个batch清理内存 | |
| if (i // batch_size) % 5 == 0: | |
| torch.cuda.empty_cache() | |
| except Exception as e: | |
| print(f"❌ GPU {gpu_id} batch {i}-{i+batch_size} 生成失败: {e}") | |
| # 添加空结果 | |
| for sample in batch: | |
| results.append({ | |
| 'user_id': sample['user_id'], | |
| 'query': sample['query'], | |
| 'profile_text': sample['profile_text'], | |
| 'nearest_cluster': cluster_id, | |
| 'generated_answer': "", | |
| 'ground_truth': sample['ground_truth'], | |
| 'gpu_id': gpu_id | |
| }) | |
| # 清理模型 | |
| del model, tokenizer | |
| torch.cuda.empty_cache() | |
| print(f"✅ GPU {gpu_id}: 完成 {len(results)} 个样本的生成") | |
| return results | |
| except Exception as e: | |
| print(f"❌ GPU {gpu_id}: 处理失败: {e}") | |
| return self._create_empty_results(samples, cluster_id, gpu_id) | |
| def _create_empty_results(self, samples: List[Dict], cluster_id: int, gpu_id: int = -1) -> List[Dict]: | |
| """创建空结果""" | |
| return [{ | |
| 'user_id': s['user_id'], | |
| 'query': s['query'], | |
| 'profile_text': s['profile_text'], | |
| 'nearest_cluster': cluster_id, | |
| 'generated_answer': "", | |
| 'ground_truth': s['ground_truth'], | |
| 'gpu_id': gpu_id | |
| } for s in samples] | |
| def split_samples_for_gpus(self, samples: List[Dict], cluster_id: int) -> List[Tuple]: | |
| """将样本分配给多个GPU""" | |
| if not samples: | |
| return [] | |
| # 计算每个GPU处理的样本数 | |
| samples_per_gpu = len(samples) // self.num_gpus | |
| remainder = len(samples) % self.num_gpus | |
| gpu_tasks = [] | |
| start_idx = 0 | |
| for gpu_id in range(self.num_gpus): | |
| # 为前几个GPU分配额外的样本(如果有余数) | |
| current_batch_size = samples_per_gpu + (1 if gpu_id < remainder else 0) | |
| end_idx = start_idx + current_batch_size | |
| if start_idx < len(samples): | |
| gpu_samples = samples[start_idx:end_idx] | |
| gpu_tasks.append((gpu_samples, cluster_id, gpu_id)) | |
| start_idx = end_idx | |
| return gpu_tasks | |
| def process_cluster_parallel(self, cluster_id: int, samples: List[Dict], | |
| max_length: int = 256, temperature: float = 0.3) -> List[Dict]: | |
| """并行处理单个聚类的样本""" | |
| print(f"\n🚀 并行处理 cluster_{cluster_id}: {len(samples)} 个样本") | |
| # 将样本分配给多个GPU | |
| gpu_tasks = self.split_samples_for_gpus(samples, cluster_id) | |
| if not gpu_tasks: | |
| return [] | |
| print(f"📋 任务分配:") | |
| for i, (gpu_samples, _, gpu_id) in enumerate(gpu_tasks): | |
| print(f" GPU {gpu_id}: {len(gpu_samples)} 个样本") | |
| all_results = [] | |
| # 使用线程池并行处理 | |
| with ThreadPoolExecutor(max_workers=self.num_gpus) as executor: | |
| future_to_gpu = {} | |
| # 提交任务到线程池 | |
| for gpu_samples, cluster_id, gpu_id in gpu_tasks: | |
| future = executor.submit( | |
| self.generate_batch_on_gpu, | |
| gpu_samples, cluster_id, gpu_id, max_length, temperature | |
| ) | |
| future_to_gpu[future] = gpu_id | |
| # 收集结果 | |
| for future in as_completed(future_to_gpu): | |
| gpu_id = future_to_gpu[future] | |
| try: | |
| results = future.result() | |
| all_results.extend(results) | |
| # 实时将结果放入队列 | |
| if results: | |
| self.results_queue.put(results) | |
| print(f"✅ GPU {gpu_id} 完成处理") | |
| except Exception as e: | |
| print(f"❌ GPU {gpu_id} 处理异常: {e}") | |
| print(f"✅ Cluster {cluster_id} 并行处理完成: {len(all_results)} 个结果") | |
| return all_results | |
| def convert_numpy_types(self, obj): | |
| """转换numpy类型为Python原生类型""" | |
| if isinstance(obj, np.integer): | |
| return int(obj) | |
| elif isinstance(obj, np.floating): | |
| return float(obj) | |
| elif isinstance(obj, np.ndarray): | |
| return obj.tolist() | |
| elif isinstance(obj, dict): | |
| return {key: self.convert_numpy_types(value) for key, value in obj.items()} | |
| elif isinstance(obj, list): | |
| return [self.convert_numpy_types(item) for item in obj] | |
| else: | |
| return obj | |
| def process_test_data(self, test_data_path: str, output_path: str, | |
| max_length: int = 256, temperature: float = 0.3): | |
| """处理测试数据的主函数""" | |
| print(f"📂 处理测试数据: {test_data_path}") | |
| # 启动实时写入线程 | |
| self.start_real_time_writer(output_path) | |
| try: | |
| # 加载测试数据 | |
| with open(test_data_path, 'r', encoding='utf-8') as f: | |
| test_data = json.load(f) | |
| # 加载或计算聚类分配 | |
| cluster_to_samples = self._load_cluster_assignments(test_data) | |
| # 按聚类处理 | |
| total_processed = 0 | |
| start_time = time.time() | |
| for cluster_id, samples in cluster_to_samples.items(): | |
| print(f"\n{'='*60}") | |
| print(f"处理聚类 {cluster_id}: {len(samples)} 个样本") | |
| cluster_start = time.time() | |
| # 并行处理聚类样本 | |
| cluster_results = self.process_cluster_parallel( | |
| cluster_id, samples, max_length, temperature | |
| ) | |
| total_processed += len(cluster_results) | |
| cluster_time = time.time() - cluster_start | |
| print(f"✅ Cluster {cluster_id} 完成,耗时: {cluster_time:.1f}s") | |
| print(f"📊 处理速度: {len(samples)/cluster_time:.1f} 样本/秒") | |
| print(f"📈 总进度: {total_processed}/{len(test_data)} ({total_processed/len(test_data)*100:.1f}%)") | |
| # 清理GPU内存 | |
| for gpu_id in range(self.num_gpus): | |
| torch.cuda.set_device(gpu_id) | |
| torch.cuda.empty_cache() | |
| gc.collect() | |
| # 等待写入完成 | |
| print("\n⏳ 等待所有结果写入完成...") | |
| time.sleep(2) # 给写入线程一些时间 | |
| total_time = time.time() - start_time | |
| print(f"\n🎉 全部处理完成!") | |
| print(f"⏱️ 总耗时: {total_time:.1f}s") | |
| print(f"⚡ 平均速度: {total_processed/total_time:.1f} 样本/秒") | |
| print(f"💾 结果保存到: {output_path}") | |
| except Exception as e: | |
| print(f"❌ 处理过程中出错: {e}") | |
| raise | |
| finally: | |
| # 停止实时写入线程 | |
| self.stop_real_time_writer() | |
| def _load_cluster_assignments(self, test_data): | |
| """加载或计算聚类分配""" | |
| assignment_path = "cluster_assignment.json" | |
| cluster_to_samples = {} | |
| if os.path.exists(assignment_path): | |
| print(f"📂 加载已有的聚类分配: {assignment_path}") | |
| with open(assignment_path, "r", encoding="utf-8") as f: | |
| assignments = {item["user_id"]: item["nearest_cluster"] for item in json.load(f)} | |
| for i, item in enumerate(test_data): | |
| user_id = item.get('name', f'user_{i}') | |
| query = item.get('input', '') | |
| if not query: | |
| continue | |
| profile_text = self.extract_user_profile(item) | |
| nearest_cluster = assignments.get(user_id, 0) | |
| if nearest_cluster not in cluster_to_samples: | |
| cluster_to_samples[nearest_cluster] = [] | |
| cluster_to_samples[nearest_cluster].append({ | |
| 'user_id': user_id, | |
| 'query': query, | |
| 'profile_text': profile_text, | |
| 'ground_truth': item.get('output', '') | |
| }) | |
| else: | |
| print("🔄 计算聚类分配...") | |
| assignments_list = [] | |
| for i, item in enumerate(tqdm(test_data, desc="Computing assignments")): | |
| user_id = item.get('name', f'user_{i}') | |
| query = item.get('input', '') | |
| if not query: | |
| continue | |
| profile_text = self.extract_user_profile(item) | |
| user_embedding = self.embedding_model.encode([profile_text])[0] | |
| nearest_cluster = self.find_nearest_cluster(user_embedding) | |
| assignments_list.append({"user_id": user_id, "nearest_cluster": int(nearest_cluster)}) | |
| if nearest_cluster not in cluster_to_samples: | |
| cluster_to_samples[nearest_cluster] = [] | |
| cluster_to_samples[nearest_cluster].append({ | |
| 'user_id': user_id, | |
| 'query': query, | |
| 'profile_text': profile_text, | |
| 'ground_truth': item.get('output', '') | |
| }) | |
| # 保存聚类分配结果 | |
| with open(assignment_path, "w", encoding="utf-8") as f: | |
| json.dump(assignments_list, f, ensure_ascii=False, indent=2) | |
| print(f"📊 聚类分布:") | |
| for cluster_id, samples in cluster_to_samples.items(): | |
| print(f" Cluster {cluster_id}: {len(samples)} 个样本") | |
| return cluster_to_samples | |
| def main(): | |
| # 设置多进程启动方法 | |
| mp.set_start_method('spawn', force=True) | |
| parser = argparse.ArgumentParser(description='多GPU并行聚类LoRA生成器') | |
| parser.add_argument('--test_data_path', type=str, required=True, | |
| help='测试数据文件路径') | |
| parser.add_argument('--output_path', type=str, default='./generated_answers.json', | |
| help='输出文件路径') | |
| parser.add_argument('--base_model', type=str, default='meta-llama/Llama-2-7b-hf', | |
| help='基础模型名称') | |
| parser.add_argument('--embedding_model', type=str, default='all-mpnet-base-v2', | |
| help='嵌入模型名称') | |
| parser.add_argument('--cluster_info_path', type=str, default='./cluster_loras/cluster_info.json', | |
| help='聚类信息文件路径') | |
| parser.add_argument('--lora_base_path', type=str, default='./cluster_loras', | |
| help='LoRA模型基础路径') | |
| parser.add_argument('--max_length', type=int, default=256, | |
| help='生成答案的最大长度') | |
| parser.add_argument('--temperature', type=float, default=0.3, | |
| help='生成温度') | |
| parser.add_argument('--num_gpus', type=int, default=None, | |
| help='使用的GPU数量(默认使用所有可用GPU)') | |
| args = parser.parse_args() | |
| # 创建输出目录 | |
| os.makedirs(os.path.dirname(args.output_path), exist_ok=True) | |
| try: | |
| # 初始化多GPU生成器 | |
| generator = MultiGPUAnswerGenerator( | |
| base_model_name=args.base_model, | |
| embedding_model=args.embedding_model, | |
| cluster_info_path=args.cluster_info_path, | |
| lora_base_path=args.lora_base_path, | |
| num_gpus=args.num_gpus | |
| ) | |
| # 处理测试数据 | |
| generator.process_test_data( | |
| args.test_data_path, | |
| args.output_path, | |
| max_length=args.max_length, | |
| temperature=args.temperature | |
| ) | |
| print(f"\n✅ 多GPU并行生成完成!") | |
| print(f"📁 结果文件: {args.output_path}") | |
| except Exception as e: | |
| print(f"❌ 程序执行失败: {e}") | |
| # 清理所有GPU内存 | |
| for gpu_id in range(torch.cuda.device_count()): | |
| torch.cuda.set_device(gpu_id) | |
| torch.cuda.empty_cache() | |
| raise | |
| if __name__ == "__main__": | |
| main() | |