Files
calculet-npu-research-archive/source/baseline-expanded/container-support/llama-modeltest.py
T

285 lines
11 KiB
Python

#!/usr/bin/env python3
import subprocess
import time
import requests
import json
import os
import sys
import argparse
from datetime import datetime
from typing import List, Tuple, Optional
# 配置常量
LOG_FILE = "llama_test_logs.csv"
CONTENT_FILE = "llama_content_logs.csv"
MAX_RETRIES = 10
RETRY_DELAY = 15
SERVER_PATH = "./build/bin/llama-server"
# 固定的测试prompt
FIXED_PROMPT = "who are you?"
def init_log_file():
"""初始化日志文件,写入CSV表头(如果文件不存在)"""
if not os.path.exists(LOG_FILE):
with open(LOG_FILE, "w") as f:
f.write("timestamp,model_path,case_path,case_name,prompt_tokens,completion_tokens,memory_usage,Decode_TPS,TTFT,Prefill_TPS,Power,result\n")
print(f"Initialized log file: {LOG_FILE}")
if not os.path.exists(CONTENT_FILE):
with open(CONTENT_FILE, "w") as f:
f.write("timestamp,model_path,case_path,case_name,prompt,content\n")
print(f"Initialized content file: {CONTENT_FILE}")
def parse_args():
parser = argparse.ArgumentParser(
description='Automatically test model (.gguf) files in case directories with fixed prompt.'
)
parser.add_argument(
"-c", "--case_path",
required=True,
help="Root directory to scan for case folders containing .gguf model files."
)
parser.add_argument(
"--port",
type=int,
default=8080,
help="llama-server port"
)
return parser.parse_args()
def discover_gguf_models(root_dir: str) -> List[Tuple[str, str, str]]:
"""
遍历根目录,查找所有包含.gguf文件的文件夹。
返回一个列表,元素为 (model_path, case_path, case_name) 元组。
case_path 是包含.gguf文件的文件夹路径,case_name 是文件夹名称。
"""
test_cases = []
if not os.path.isdir(root_dir):
print(f"Error: Provided root_dir '{root_dir}' is not a valid directory.")
return test_cases
print(f"Scanning root directory: {root_dir}")
for item in os.scandir(root_dir):
if item.is_dir():
case_path = item.path
case_name = item.name
gguf_files = []
# 遍历文件夹内所有文件,查找.gguf文件
for file_item in os.scandir(case_path):
if file_item.is_file() and file_item.name.endswith('.gguf'):
gguf_files.append(file_item.path)
if gguf_files:
# 如果有多个.gguf文件,可以选择第一个或让用户选择
# 这里我们选择第一个找到的.gguf文件
model_path = gguf_files[0]
if len(gguf_files) > 1:
print(f" Warning: Found {len(gguf_files)} .gguf files in '{case_name}', using: {os.path.basename(model_path)}")
test_cases.append((model_path, case_path, case_name))
print(f" Found: Case='{case_name}', Model='{os.path.basename(model_path)}'")
else:
print(f" Skipping folder '{case_name}': No .gguf files found.")
print(f"Total discovered test cases: {len(test_cases)}\n")
return test_cases
def start_server(model_path: str, case_path: str, port: int) -> Optional[subprocess.Popen]:
"""启动llama-server进程,返回subprocess.Popen对象。如果启动失败,返回None。"""
cmd = ["sudo", SERVER_PATH, "-m", str(model_path), "--case", str(case_path), "--port", str(port), "-np", str(1), "--verbose", "--no-warmup"]
print(f"Starting server with: Model='{model_path}', Case='{case_path}'")
print(f"Command: {' '.join(cmd)}")
try:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(1) # 稍等片刻让进程启动
return process
except Exception as e:
print(f"Failed to start server: {e}")
return None
def health_check(health_url: str) -> bool:
"""健康检查,重试直到服务就绪或超时"""
for i in range(1, MAX_RETRIES + 1):
try:
response = requests.get(health_url, timeout=15)
if response.status_code == 200:
print(f"✔ llama-server is ready (attempt {i}/{MAX_RETRIES})")
return True
except requests.exceptions.RequestException as e:
pass # 忽略连接错误
print(f" Waiting for service to be ready (attempt {i}/{MAX_RETRIES}, retry in {RETRY_DELAY}s)...")
time.sleep(RETRY_DELAY)
print("✘ llama-server startup timed out")
return False
def send_request(prompt: str, model_path: str, api_url: str) -> Optional[dict]:
"""发送对话请求,提取并返回性能数据。如果失败,返回None。"""
request_body = {
"model": model_path,
"messages": [{"role": "user", "content": prompt}],
"stream": False
}
print(f" Sending request with prompt: '{prompt[:50]}...'" if len(prompt) > 50 else f" Sending request: '{prompt}'")
try:
response = requests.post(api_url, json=request_body, timeout=150)
response.raise_for_status()
data = response.json()
except requests.exceptions.Timeout:
print(f" ✗ Request timed out.")
return None
except requests.exceptions.RequestException as e:
print(f" Request failed: {e}")
return None
except json.JSONDecodeError as e:
print(f" JSON decode failed: {e}")
return None
# 提取数据
usage = data.get("usage", {})
timings = data.get("timings", {})
prompt_tokens = usage.get("prompt_tokens", 0)
completion_tokens = usage.get("completion_tokens", 0)
decode_tps = timings.get("predicted_per_second", 0.0)
predicted_ms = timings.get("predicted_ms", 0.0)
prompt_ms = timings.get("prompt_ms", 0.0)
ttft = predicted_ms + prompt_ms
prefill_tps = timings.get("prompt_per_second", 0.0)
message = data.get("choices", [{}])[0].get("message", {})
content = message.get("reasoning_content") or message.get("content", "")
return {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"memory_usage": 0,
"decode_tps": decode_tps,
"ttft": ttft,
"prefill_tps": prefill_tps,
"Power": 0,
"prompt": prompt,
"content": content,
"model_path": model_path
}
def log_data(metrics: dict, case_path: str, case_name: str, result: str):
"""记录数据到CSV文件"""
if metrics is None:
print("No valid data, skipping logging.")
return
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(LOG_FILE, "a") as f:
row = f"{timestamp},{metrics['model_path']},{case_path},{case_name},{metrics['prompt_tokens']},{metrics['completion_tokens']},{metrics['memory_usage']:.2f},{metrics['decode_tps']:.2f},{metrics['ttft']:.2f},{metrics['prefill_tps']:.2f},{metrics['Power']:.1f},{result}\n"
f.write(row)
with open(CONTENT_FILE, "a") as f:
# 对内容中的换行符和逗号进行处理,用空格替换换行,用占位符替换逗号以确保CSV格式正确
DELIMITER = '\t'
# safe_content = metrics['content'].replace('\n', ' ').replace(',', ';')
# safe_prompt = metrics['prompt'].replace('\n', ' ').replace(',', ';')
row = f"{timestamp},{metrics['model_path']}{DELIMITER}{case_path}{DELIMITER}{case_name}{DELIMITER}{metrics['prompt']}{DELIMITER}{metrics['content']}\n"
f.write(row)
def main():
"""主函数,执行完整流程"""
print("=== Starting Automated llama-server Test Suite (Fixed Prompt) ===")
# 1. 初始化日志文件
init_log_file()
# 2. 解析参数
args = parse_args()
# 设置固定prompt
#fixed_prompt = args.prompt
print(f"Using fixed prompt: '{FIXED_PROMPT}'")
# 发现所有包含.gguf文件的文件夹
test_cases = discover_gguf_models(args.case_path)
if not test_cases:
print("No folders with .gguf files found. Exiting.")
sys.exit(1)
overall_results = []
# 3. 遍历每个测试组合
for model_path, case_path, case_name in test_cases:
server_process = None
combination_result = "FAIL" # 默认失败
print(f"\n{'='*60}")
print(f"Testing Case:")
print(f" Case Name: {case_name}")
print(f" Case Path: {case_path}")
print(f" Model: {model_path}")
print(f"{'='*60}")
try:
# 启动服务器
server_process = start_server(model_path, case_path, args.port)
if not server_process:
raise RuntimeError(f"Failed to start server for model '{model_path}' and case '{case_path}'")
# 健康检查
health_url = f"http://localhost:{args.port}/health"
api_url = f"http://localhost:{args.port}/v1/chat/completions"
if not health_check(health_url):
raise RuntimeError("Health check failed. Server may not have started correctly.")
# 使用固定prompt发送请求
print(f"\n Sending test request with fixed prompt...")
metrics = send_request(FIXED_PROMPT, model_path, api_url)
if metrics:
# 记录请求的详细日志
log_data(metrics, case_path, case_name, "REQUEST_OK")
combination_result = "PASS"
print(f"\n ✔ Case '{case_name}' TEST PASSED: Request succeeded.")
else:
combination_result = "FAIL"
print(f"\n ✗ Case '{case_name}' TEST FAILED: Request failed.")
# 记录该组合的最终结果(汇总行)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# with open(LOG_FILE, "a") as f:
# summary_row = f"{timestamp},{model_path},{case_path},{case_name},,,,,,,,{combination_result}\n"
# f.write(summary_row)
overall_results.append((model_path, case_path, case_name, combination_result))
except Exception as e:
print(f"\n ✗ An error occurred during testing of case '{case_name}': {e}")
combination_result = "ERROR"
overall_results.append((model_path, case_path, case_name, f"ERROR: {e}"))
finally:
# 停止当前组合的服务器进程
if server_process and server_process.poll() is None:
print(f" Stopping server for case '{case_name}'...")
server_process.terminate()
try:
server_process.wait(timeout=5)
except subprocess.TimeoutExpired:
server_process.kill()
# 短暂等待,避免端口占用影响下一个测试
time.sleep(2)
# 4. 打印最终汇总报告
print(f"\n{'='*60}")
print("TEST SUITE SUMMARY:")
print(f"{'='*60}")
for model_path, case_path, case_name, result in overall_results:
status_symbol = "✔" if result == "PASS" else "✗"
print(f"{status_symbol} Case: {case_name:50} | Result: {result}")
# 判断整个测试套件是否全部通过
all_passed = all(result == "PASS" for _, _, _, result in overall_results)
if all_passed:
print("\n✔ All test cases PASSED.")
sys.exit(0)
else:
print("\n✗ Some test cases FAILED or encountered ERRORS.")
sys.exit(1)
if __name__ == "__main__":
main()