Files
xc_validation_strategy_gguf/main.py
2026-07-14 16:44:33 +08:00

520 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
xc_validation_strategy_gguf — 主入口
GGUF 模型下载 + 验证任务提交流水线(部署框架与 xc_validation_strategy 一致)。
启动后运行流水线:批量创建 GGUF 模型下载任务(最大并发 8
每个模型下载成功后立即提交 hygon / bi150 两个验证任务;
同时暴露 /healthK8s 探活)和 /status运行状态
"""
import json
import os
import re
import signal
import threading
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Set
import requests
# ══════════════════════════════════════════════════════════
# 配置
# ══════════════════════════════════════════════════════════
BASE_URL = os.environ.get("BASE_URL", "https://modelhub.org.cn")
LOGIN_ENDPOINT = "/adminApi/user/login"
CREATE_DOWNLOAD_TASK_ENDPOINT = "/adminApi/async/task/model-download-task"
SUBMIT_TEST_TASK_ENDPOINT = "/adminApi/async/task/create-contest-task"
# 登录账号:启动时优先用账号密码换取新 token流水线运行时间长预设 token 可能过期)
USER_ACCOUNT = "zhoushasha@4paradigm.com"
USER_PASSWORD = "ganshenme0"
# 通过 curl -X POST https://modelhub.org.cn/adminApi/user/login 获取后填入(登录失败时的回退)
AUTH_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyQWNjb3VudCI6Inpob3VzaGFzaGEiLCJpZCI6MTQsInVzZXJSb2xlIjoibGVhZGVyYm9hcmQiLCJleHAiOjE3ODQ1NDc1NDYsImlhdCI6MTc4Mzk0Mjc0Nn0.ZcOqcrfI22LPi4mGMnt164nZGhi61ZxtJGYsoO7fZdM"
CONTEST_API_TOKEN = "ef1ef82f3c9efee413d602345fbe224d"
HF_TOKEN = "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"
CONTRIBUTORS = "zhoushasha"
TASK_TYPE = "text-generation"
STRATEGY_ID = os.environ.get("STRATEGY_ID", "") # 平台自动注入,无需修改
MAX_CONCURRENT_DOWNLOADS = 8 # 同时下载的模型数上限
CHECK_INTERVAL_SECONDS = 10 # 下载状态轮询间隔(秒)
HTTP_HOST = "0.0.0.0"
HTTP_PORT = 8080
# ══════════════════════════════════════════════════════════
# 模型列表
# ══════════════════════════════════════════════════════════
ALL_MODEL_IDS = [
"mradermacher/Qwen2.5-3B-instruct-argus-v3-GGUF",
"mradermacher/NeuralsirkrishnaShadow_OgnoExperiment27-GGUF",
"mradermacher/GREEN-RadLlama2-7b-GGUF",
"mradermacher/snakmodel-7b-instruct-GGUF",
"mradermacher/BioMistral-7B-Starling-SLERP-GGUF",
"mradermacher/Marco-Llama-3.2-3B-GGUF",
"mradermacher/Qwen2.5-1.5B-Instruct-Open-R1-GRPO-GGUF",
"mradermacher/Qwen2.5-0.5B-Distill-Fast-GGUF",
"mradermacher/chomsky_16_bit_model-GGUF",
"mradermacher/Mistral-7B-DFT-GGUF",
"mradermacher/mergekit-task_arithmetic-qjeuqjw-GGUF",
"mradermacher/M7Yamshadowexperiment28_Experiment27Inex12-GGUF",
"mradermacher/YamshadowStrangemerges_32_Experiment28Inex12-GGUF",
"mradermacher/MeliodasPercival_01_Experiment29Pastiche-GGUF",
"mradermacher/Llama-3-6B-v0-GGUF",
"mradermacher/Excalibur-7b-DPO-GGUF",
"mradermacher/Llama-2-7b-Indian-Law-GGUF",
"mradermacher/StarlingHermes-2.5-Mistral-7B-slerp-GGUF",
"mradermacher/DeepThinker-7B-Sce-v1-GGUF",
"mradermacher/Alif-Llama-EXP2-GGUF",
"mradermacher/qwen-2.5-1.5B-Rasa-GGUF",
"mradermacher/German_RAG-PHI-3.5-MINI-4B-MERGED-HESSIAN-AI-GGUF",
"mradermacher/MFANN-phigments-slerp-V3.2-GGUF",
"mradermacher/AdityaGPT-GGUF",
"mradermacher/Jaja-small-v4-GGUF",
"mradermacher/Jaja-medium-v1-GGUF",
"mradermacher/astrollama-2-7b-base_abstract-GGUF",
"mradermacher/Llasagna-v0.1-GGUF",
"mradermacher/DeepThinker-v-GGUF",
"mradermacher/Jaja-small-v3-GGUF",
"mradermacher/Llama-3.2-1B-FC-v1.2-think-GGUF",
"mradermacher/Deepseek-Qwen2.5-1.5B-Redistil-GGUF",
]
# 去重(保持原有顺序)
_seen = set()
_deduplicated = []
for _mid in ALL_MODEL_IDS:
if _mid not in _seen:
_deduplicated.append(_mid)
_seen.add(_mid)
ALL_MODEL_IDS = _deduplicated
print(f"[INFO] 去重后模型数量: {len(ALL_MODEL_IDS)}", flush=True)
HEADERS = {"Content-Type": "application/json"}
# ══════════════════════════════════════════════════════════
# 全局状态(供 /status 展示)
# ══════════════════════════════════════════════════════════
_state = {
"strategy_id": STRATEGY_ID,
"phase": "starting", # starting | running | done | error
"total": len(ALL_MODEL_IDS),
"downloading": [], # 当前正在下载的模型
"download_success": 0,
"download_failed": 0,
"submitted": 0, # 成功提交的验证任务数hygon + bi150
"submit_failed": 0,
"started_at": None,
"finished_at": None,
}
_shutdown = threading.Event()
# ══════════════════════════════════════════════════════════
# HTTP 服务
# ══════════════════════════════════════════════════════════
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self._json({"status": "ok"})
elif self.path == "/status":
self._json(_state)
else:
self._json({"error": "not found"}, 404)
def _json(self, body: dict, code: int = 200):
payload = json.dumps(body, default=str).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, fmt, *args):
print(f"[http] {self.address_string()} {fmt % args}", flush=True)
def _run_http():
server = ThreadingHTTPServer((HTTP_HOST, HTTP_PORT), Handler)
server.timeout = 1
print(f"[http] 监听 {HTTP_HOST}:{HTTP_PORT}", flush=True)
while not _shutdown.is_set():
server.handle_request()
server.server_close()
print("[http] 已关闭", flush=True)
# ══════════════════════════════════════════════════════════
# 工具函数:生成模型文件名
# ══════════════════════════════════════════════════════════
def get_model_filename(model_id: str) -> str:
"""
从 model_id 生成标准 GGUF 模型文件名。
规则:
- 移除组织名(/ 前部分)
- 处理 '_-_' 分割(保留原有逻辑)
- 移除末尾 '-GGUF'(不区分大小写)
- 若移除后以 -i1, -i2, ..., -i99 结尾:
→ 替换为 .i1, .i2, ... 并添加 '-Q4_0.gguf'
否则:
→ 直接添加 '.f16.gguf'
示例:
'mradermacher/Qwen3-8B-makisu-v2.0.1-i1-GGUF'
'Qwen3-8B-makisu-v2.0.1.i1-Q4_0.gguf'
'QuantFactory/Apollo2-9B-GGUF'
'Apollo2-9B.f16.gguf'
"""
# 1. 提取模型名部分(/ 后)
base_name = model_id.split("/")[-1]
# 2. 处理 '_-_' 分割
if '_-_' in base_name:
base_name = base_name.split('_-_')[-1]
# 3. 移除末尾的 -GGUF不区分大小写
if base_name.lower().endswith("-gguf"):
base_name = base_name[:-5]
# 4. 检查是否以 -i<数字> 结尾(支持 i1i99 等)
match = re.search(r'-i(\d+)$', base_name)
if match:
number = match.group(1)
base_name = base_name[:match.start()] + f".i{number}-Q4_0.gguf"
else:
base_name = base_name + ".f16.gguf"
return base_name
# ══════════════════════════════════════════════════════════
# 登录获取 token失败时回退到预设 AUTH_TOKEN
# ══════════════════════════════════════════════════════════
def login() -> str:
payload = {"userAccount": USER_ACCOUNT, "userPassword": USER_PASSWORD}
print("[login] 正在登录...", flush=True)
try:
resp = requests.post(BASE_URL + LOGIN_ENDPOINT, headers=HEADERS, json=payload, timeout=15)
data = resp.json()
if resp.status_code == 200 and data.get("code") == 0:
print("[login] 登录成功", flush=True)
return data["data"]["token"]
print(f"[login] 登录失败: {data.get('message')},回退使用预设 Token", flush=True)
except Exception as e:
print(f"[login] 登录异常: {e},回退使用预设 Token", flush=True)
return AUTH_TOKEN
# ══════════════════════════════════════════════════════════
# 创建单个模型的下载任务
# ══════════════════════════════════════════════════════════
def create_download_task(token: str, model_id: str) -> bool:
filename = get_model_filename(model_id)
auth_headers = {**HEADERS, "Authorization": f"Bearer {token}"}
payload = {
"allowPatterns": [filename],
"hfToken": HF_TOKEN,
"modelId": model_id,
"source": "HUGGING_FACE",
"stillDownloadAlreadySuccessDownloadedModel": False
}
print(f"📥 创建下载任务: {model_id}{filename}", flush=True)
try:
resp = requests.post(BASE_URL + CREATE_DOWNLOAD_TASK_ENDPOINT, headers=auth_headers, json=payload, timeout=15)
if resp.status_code == 200:
data = resp.json()
if data.get("code") == 0:
print(f"✅ 下载任务已提交: {model_id}", flush=True)
return True
else:
print(f"⚠️ 下载任务业务失败 ({model_id}): {data.get('message')}", flush=True)
return False
else:
print(f"❌ HTTP 错误 ({model_id}): {resp.status_code} - {resp.text}", flush=True)
return False
except Exception as e:
print(f"💥 创建下载任务异常 ({model_id}): {e}", flush=True)
return False
# ══════════════════════════════════════════════════════════
# 查询单个模型的最新下载任务状态
# ══════════════════════════════════════════════════════════
def check_model_status(token: str, model_id: str) -> str:
"""
返回状态: 'WAITING', 'RUNNING', 'SUCCESS', 'FAILED', 'UNKNOWN'
"""
url = BASE_URL + CREATE_DOWNLOAD_TASK_ENDPOINT
auth_headers = {**HEADERS, "Authorization": f"Bearer {token}"}
params = {"modelId": model_id, "current": 1, "pageSize": 1}
try:
resp = requests.get(url, headers=auth_headers, params=params, timeout=10)
if resp.status_code != 200:
return "UNKNOWN"
data = resp.json()
if data.get("code") != 0:
return "UNKNOWN"
records = data.get("data", {}).get("records", [])
if not records:
return "UNKNOWN"
status = records[0].get("status", "UNKNOWN").upper()
return status
except Exception as e:
print(f"⚠️ 查询状态异常 ({model_id}): {e}", flush=True)
return "UNKNOWN"
# ══════════════════════════════════════════════════════════
# 提交单个模型的测试任务 hygon
# ══════════════════════════════════════════════════════════
def submit_test_task(token: str, model_id: str) -> bool:
auth_headers = {**HEADERS, "Authorization": f"Bearer {token}"}
model_filename = get_model_filename(model_id)
gpu_type = "hygon_k100-ai"
config_content = f"""docker_image: git.modelhub.org.cn:9443/enginex-hygon/hygon-llama.cpp:b7516
nv_docker_image: harbor-contest.4pd.io/luxinlong02/llama-cpp:b7003-cuda-full-12.3
framework: llamacpp
storage: gpfs
modelhub_options:
srcRelativePath: leaderboard/modelHubXC/{model_id}
mountPoint: /model
api: completion
temperature: 0
repetition_penalty: 1.1
top_p: 0.9
max_model_len: 4096
sut_config:
gpu_num: 1
values:
command: ['/app/llama-server','--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '20','--n-gpu-layers', '999', '--prio', '3', '--min_p', '0.01', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off']
ref_config:
gpu_num: 1
values:
command: ['/workspace/llama.cpp/build/bin/llama-server','--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '20', '--n-gpu-layers', '999', '--prio', '3', '--min_p', '0.01', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off']
"""
task_data = {
"contestApiToken": CONTEST_API_TOKEN,
"contributors": CONTRIBUTORS,
"gpuTypes": [gpu_type],
"taskType": TASK_TYPE,
"modelId": model_id,
"strategyId": STRATEGY_ID, # 平台要求
"submissionConfig": [{
"config": config_content,
"gpuType": gpu_type,
"taskType": TASK_TYPE
}]
}
print(f"📤 提交测试任务 (hygon): {model_id}", flush=True)
try:
resp = requests.post(BASE_URL + SUBMIT_TEST_TASK_ENDPOINT, json=task_data, headers=auth_headers, timeout=15)
if resp.status_code == 200:
result = resp.json()
if result.get("code") == 0:
task_id = result.get("data", {}).get("taskId")
print(f"✅ 测试任务提交成功! Task ID: {task_id}", flush=True)
return True
else:
print(f"❌ 测试任务业务错误: {result.get('message')}", flush=True)
return False
else:
print(f"❌ 测试任务 HTTP 错误: {resp.status_code} - {resp.text}", flush=True)
return False
except Exception as e:
print(f"💥 提交测试任务异常 ({model_id}): {e}", flush=True)
return False
# ══════════════════════════════════════════════════════════
# 提交单个模型的测试任务 bi150
# ══════════════════════════════════════════════════════════
def submit_test_task_bi150(token: str, model_id: str) -> bool:
auth_headers = {**HEADERS, "Authorization": f"Bearer {token}"}
model_filename = get_model_filename(model_id)
gpu_type = "Iluvatar_bi-150"
config_content = f"""docker_image: git.modelhub.org.cn:9443/enginex-iluvatar/iluvatar-llama.cpp:b7516-bi150
nv_docker_image: harbor-contest.4pd.io/luxinlong02/llama-cpp:b7003-cuda-full-12.3
framework: llamacpp
storage: gpfs
modelhub_options:
srcRelativePath: leaderboard/modelHubXC/{model_id}
mountPoint: /model
max_model_len: 4096
sut_config:
gpu_num: 1
values:
command: ['/app/llama-server','--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '20','--n-gpu-layers','128', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off', '--no-mmap', '--sync-to-temp']
ref_config:
gpu_num: 1
values:
command: ['/workspace/llama.cpp/build/bin/llama-server','--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '20','--n-gpu-layers','128', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off']
"""
task_data = {
"contestApiToken": CONTEST_API_TOKEN,
"contributors": CONTRIBUTORS,
"gpuTypes": [gpu_type],
"taskType": TASK_TYPE,
"modelId": model_id,
"strategyId": STRATEGY_ID, # 平台要求
"submissionConfig": [{
"config": config_content,
"gpuType": gpu_type,
"taskType": TASK_TYPE
}]
}
print(f"📤 提交测试任务 (bi150): {model_id}", flush=True)
try:
resp = requests.post(BASE_URL + SUBMIT_TEST_TASK_ENDPOINT, json=task_data, headers=auth_headers, timeout=15)
if resp.status_code == 200:
result = resp.json()
if result.get("code") == 0:
task_id = result.get("data", {}).get("taskId")
print(f"✅ 测试任务提交成功! Task ID: {task_id}", flush=True)
return True
else:
print(f"❌ 测试任务业务错误: {result.get('message')}", flush=True)
return False
else:
print(f"❌ 测试任务 HTTP 错误: {resp.status_code} - {resp.text}", flush=True)
return False
except Exception as e:
print(f"💥 提交测试任务异常 ({model_id}): {e}", flush=True)
return False
# ══════════════════════════════════════════════════════════
# 业务逻辑:动态流水线(下载 → 提交验证任务)
# ══════════════════════════════════════════════════════════
def _run_worker():
_state["started_at"] = datetime.utcnow().isoformat()
_state["phase"] = "running"
try:
token = login()
except Exception as e:
print(f"[worker] 登录失败: {e}", flush=True)
_state["phase"] = "error"
return
pending_models = list(ALL_MODEL_IDS) # 尚未开始下载的模型
active_models: Set[str] = set() # 当前正在下载的模型
completed_results = {} # model_id -> status
print(f"🚀 总共 {len(pending_models)} 个模型待下载。最大并发数: {MAX_CONCURRENT_DOWNLOADS}\n", flush=True)
while (pending_models or active_models) and not _shutdown.is_set():
# 1. 检查活跃任务状态
for model_id in list(active_models):
if _shutdown.is_set():
break
status = check_model_status(token, model_id)
if status in ("SUCCESS", "FAILED"):
completed_results[model_id] = status
active_models.remove(model_id)
print(f"⏹️ {model_id} 完成,状态: {status}", flush=True)
if status == "SUCCESS":
_state["download_success"] += 1
# 下载成功后立即提交该模型的验证任务
if submit_test_task(token, model_id):
_state["submitted"] += 1
print(f"🧪 已为 {model_id} 提交 hygon 验证任务", flush=True)
else:
_state["submit_failed"] += 1
print(f"⚠️ {model_id} hygon 验证任务提交失败", flush=True)
if submit_test_task_bi150(token, model_id):
_state["submitted"] += 1
print(f"🧪 已为 {model_id} 提交 bi150 验证任务", flush=True)
else:
_state["submit_failed"] += 1
print(f"⚠️ {model_id} bi150 验证任务提交失败", flush=True)
else:
_state["download_failed"] += 1
# 2. 补充新任务(最多补到 MAX_CONCURRENT_DOWNLOADS 个)
while len(active_models) < MAX_CONCURRENT_DOWNLOADS and pending_models and not _shutdown.is_set():
next_model = pending_models.pop(0)
if create_download_task(token, next_model):
active_models.add(next_model)
print(f"▶️ 启动下载: {next_model} (当前活跃: {len(active_models)})", flush=True)
else:
# 创建失败也视为完成(避免卡住)
completed_results[next_model] = "CREATE_FAILED"
_state["download_failed"] += 1
print(f"❌ 创建失败: {next_model}", flush=True)
_state["downloading"] = sorted(active_models)
# 3. 稍作等待,避免频繁查询(可被 shutdown 信号打断)
if active_models or pending_models:
_shutdown.wait(CHECK_INTERVAL_SECONDS)
print("\n✅ 所有模型处理完毕!\n", flush=True)
# 4. 收集所有成功下载的模型ID并写入结果文件
success_models = [
mid for mid, status in completed_results.items()
if status == "SUCCESS"
]
try:
with open("downloaded_success_models.txt", "w", encoding="utf-8") as f:
for mid in success_models:
f.write(f"{mid}\n")
except Exception:
pass
print("🎉 下载成功的模型ID列表", flush=True)
print("[", flush=True)
for mid in success_models:
print(f' "{mid}",', flush=True)
print("]", flush=True)
_state["downloading"] = []
_state["finished_at"] = datetime.utcnow().isoformat()
_state["phase"] = "done"
print(
f"[worker] 完成 download_success={_state['download_success']} "
f"download_failed={_state['download_failed']} "
f"submitted={_state['submitted']} submit_failed={_state['submit_failed']}",
flush=True,
)
# 流水线完成后继续保持进程存活,等待平台停止
# ══════════════════════════════════════════════════════════
# 入口
# ══════════════════════════════════════════════════════════
def _handle_signal(signum, _frame):
print(f"[main] 收到信号 {signum},正在关闭...", flush=True)
_shutdown.set()
def main():
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
# HTTP 服务线程
http_thread = threading.Thread(target=_run_http, daemon=False)
http_thread.start()
# 流水线线程
worker_thread = threading.Thread(target=_run_worker, daemon=True)
worker_thread.start()
# 主线程等待 shutdown
_shutdown.wait()
print("[main] 等待 HTTP 服务关闭...", flush=True)
http_thread.join(timeout=5)
print("[main] 退出", flush=True)
if __name__ == "__main__":
main()