Files
new-pipeline/modelhub_pipeline_resumable.py

1121 lines
36 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.

import argparse
import os
import re
import json
import random
import threading
import queue
from typing import Dict, Tuple, List, Optional, Set
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# ============================================================
# 1. 基础配置
# ============================================================
BASE_URL = "https://modelhub.org.cn"
LOGIN_ENDPOINT = "/adminApi/user/login"
ADD_ADAPT_TASK_ENDPOINT = "/api/adapt/task/add"
MODELHUB_DB_CHECK_ENDPOINT = "/api/computility/models/list/page/vo"
TASK_TYPE = "text-generation"
FRAMEWORK = "llamacpp"
# 按当前策略部署约定ModelHub 登录凭据固定写入策略代码。
USER_ACCOUNT = "1326356020@qq.com"
USER_PASSWORD = "4pdpassword"
# 不使用手动 token统一用账号密码登录获取 token
MODELHUB_TOKEN = ""
# 平台环境变量契约:提交任务时必须携带当前策略 ID。
STRATEGY_ID = os.environ["STRATEGY_ID"]
# ============================================================
# 2. Hugging Face / hf-mirror 筛选配置
# ============================================================
ORG_NAME = "mradermacher"
HF_API_URL = "https://hf-mirror.com/api/models"
PER_PAGE = int(os.getenv("HF_PER_PAGE", "100"))
START_PAGE = int(os.getenv("HF_START_PAGE", "1"))
END_PAGE = int(os.getenv("HF_END_PAGE", "0")) # 0 表示持续翻页直到末页
REQUEST_DELAY = 1 # 每次请求的基础延迟(秒),可根据情况调整
RANDOM_DELAY_RANGE = (0.5, 2) # 随机延迟范围(秒),避免固定间隔被识别
RETRY_TIMES = 3 # 429错误重试次数
RETRY_DELAY = 5 # 每次重试的等待时间(秒)
MAX_FILE_SIZE_GB = 12
# ============================================================
# 3. 提交与轮询配置
# ============================================================
# 如果所有模型都想强制用某种 GPU就填
# FORCE_TARGET_GPU = "Ascend_910-b4"
# 如果保留 None则 i1/i2/... 模型走 Ascend_910-b4其他走 Mthreads_s4000
FORCE_TARGET_GPU = None
SUBMIT_INTERVAL_SECONDS = 2
TASK_LIMIT_CODE = 60007
POLL_INTERVAL_SECONDS = 120
MAX_LIMIT_RETRY_TIMES = None # None 表示一直等到有空位
PIPELINE_MODE = os.getenv("PIPELINE_MODE", "continuous").strip().lower()
SCAN_INTERVAL_SECONDS = int(os.getenv("PIPELINE_SCAN_INTERVAL_SECONDS", "1800"))
MAX_SCAN_PAGES = int(os.getenv("HF_MAX_PAGES", "0")) # 0 表示不限制
RETRY_FAILED = os.getenv("PIPELINE_RETRY_FAILED", "true").strip().lower() in {
"1", "true", "yes", "on"
}
SHUTDOWN_TIMEOUT_SECONDS = 25
DEBUG_PRINT_PAYLOAD = False
# 是否开启断点续跑:已经成功提交过的模型,下次运行自动跳过
RESUME = True
# ============================================================
# 断点续跑 / 重跑配置
# ============================================================
# run_mode 说明:
# resume优先复用已有步骤产物缺失时才运行该步骤。适合 Step 4 报错后接着跑。
# fresh :从 start_step 开始重跑并覆盖对应步骤产物start_step 之前的步骤读取已有产物。
# 命令行参数优先级高于这里的默认值,例如:
# python modelhub_pipeline_resumable.py --start-step 4 --run-mode resume
DEFAULT_RUN_MODE = os.getenv("PIPELINE_RUN_MODE", "resume")
DEFAULT_START_STEP = int(os.getenv("PIPELINE_START_STEP", "1"))
DEFAULT_FORCE_RERUN_STEPS = os.getenv("PIPELINE_FORCE_RERUN_STEPS", "")
OUTPUT_DIR = "pipeline_outputs"
ALL_MODELS_FILE = os.path.join(OUTPUT_DIR, "01_all_models.txt")
SIZE_FILTERED_FILE = os.path.join(OUTPUT_DIR, "02_size_filtered_models.txt")
NOT_IN_DB_FILE = os.path.join(OUTPUT_DIR, "03_not_in_db_models.txt")
SUBMITTED_FILE = os.path.join(OUTPUT_DIR, "04_submitted_models.txt")
FAILED_FILE = os.path.join(OUTPUT_DIR, "05_failed_models.txt")
SUMMARY_FILE = os.path.join(OUTPUT_DIR, "summary.json")
BASE_HEADERS = {
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"Origin": BASE_URL,
"Referer": BASE_URL + "/",
"User-Agent": "Mozilla/5.0",
}
_shutdown_event = threading.Event()
class ShutdownRequested(Exception):
"""平台请求策略进程优雅停止。"""
def request_shutdown() -> None:
_shutdown_event.set()
def reset_shutdown() -> None:
_shutdown_event.clear()
def check_shutdown() -> None:
if _shutdown_event.is_set():
raise ShutdownRequested("收到停机信号")
def interruptible_sleep(seconds: float) -> None:
if _shutdown_event.wait(max(0, seconds)):
raise ShutdownRequested("收到停机信号")
# ============================================================
# 4. 通用工具
# ============================================================
def ensure_output_dir() -> None:
os.makedirs(OUTPUT_DIR, exist_ok=True)
def write_model_list(path: str, models: List[str]) -> None:
with open(path, "w", encoding="utf-8") as f:
for model in models:
f.write(f'"{model}",\n')
def append_model(path: str, model: str) -> None:
with open(path, "a", encoding="utf-8") as f:
f.write(f'"{model}",\n')
def load_model_set(path: str) -> Set[str]:
if not os.path.exists(path):
return set()
with open(path, "r", encoding="utf-8") as f:
content = f.read()
quoted_items = re.findall(r'["\']([^"\']+)["\']', content)
if quoted_items:
return set(x.strip() for x in quoted_items if x.strip())
result = set()
for line in content.splitlines():
item = line.strip().strip(",").strip('"').strip("'").strip()
if item:
result.add(item)
return result
def load_model_list(path: str) -> List[str]:
"""读取模型列表,保持顺序并去重。"""
if not os.path.exists(path):
raise FileNotFoundError(
f"找不到断点文件: {path}。如果要从中间步骤继续,请先确认前置步骤已成功生成该文件。"
)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
quoted_items = re.findall(r'["\']([^"\']+)["\']', content)
if quoted_items:
raw_items = [x.strip() for x in quoted_items if x.strip()]
else:
raw_items = []
for line in content.splitlines():
item = line.strip().strip(",").strip('"').strip("'").strip()
if item:
raw_items.append(item)
seen = set()
result = []
for item in raw_items:
if item not in seen:
result.append(item)
seen.add(item)
return result
def parse_bool_env(value: str, default: bool = False) -> bool:
if value is None or value == "":
return default
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
def parse_step_set(value: str) -> Set[int]:
result: Set[int] = set()
if not value:
return result
for item in value.split(","):
item = item.strip()
if not item:
continue
try:
step = int(item)
except ValueError:
raise ValueError(f"PIPELINE_FORCE_RERUN_STEPS 里包含非法步骤: {item}")
if step not in {1, 2, 3}:
raise ValueError("可强制重跑的步骤只能是 1、2、3Step 4 每次都会按断点提交剩余模型。")
result.add(step)
return result
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="HF / hf-mirror -> ModelHub 断点续跑脚本"
)
parser.add_argument(
"--run-mode",
choices=["resume", "fresh"],
default=DEFAULT_RUN_MODE,
help="resume=复用已有步骤结果fresh=从 start-step 开始重跑。默认读取 PIPELINE_RUN_MODE 或 resume。",
)
parser.add_argument(
"--start-step",
type=int,
choices=[1, 2, 3, 4],
default=DEFAULT_START_STEP,
help="从第几步开始。Step 4 报错后继续跑用 --start-step 4。默认读取 PIPELINE_START_STEP 或 1。",
)
parser.add_argument(
"--force-rerun-steps",
default=DEFAULT_FORCE_RERUN_STEPS,
help="在 resume 模式下强制重跑指定前置步骤,例如 '2,3'。Step 4 不需要写入这里。",
)
parser.add_argument(
"--reset-submitted",
action="store_true",
help="清空 04_submitted_models.txt。谨慎使用会导致已成功提交过的模型不再被跳过。",
)
parser.add_argument(
"--reset-failed",
action="store_true",
help="清空 05_failed_models.txt。适合修复参数后重新统计失败模型。",
)
parser.add_argument(
"--skip-failed",
action="store_true",
help="跳过 05_failed_models.txt 中记录过失败的模型。默认不跳过,便于修复问题后自动重试失败模型。",
)
args = parser.parse_args()
args.force_rerun_steps = parse_step_set(args.force_rerun_steps)
return args
def should_run_step(step: int, output_file: str, args: argparse.Namespace) -> bool:
"""
判断 Step 1-3 是否需要执行。
resume如果产物存在就读取不存在才执行force-rerun-steps 可强制执行。
fresh start_step 之前读取已有产物,从 start_step 开始重新执行并覆盖产物。
"""
if step in args.force_rerun_steps:
return True
if args.run_mode == "fresh":
return step >= args.start_step
# resume mode
if step < args.start_step:
return False
return not os.path.exists(output_file)
def print_step_loaded(step: int, path: str, models: List[str]) -> None:
print(f"Step {step}/4: 跳过执行,读取已有断点文件: {path}")
print(f"读取到 {len(models)} 个模型")
def remove_file_if_exists(path: str) -> None:
if os.path.exists(path):
os.remove(path)
print(f"已清空: {path}")
def create_hf_session() -> requests.Session:
session = requests.Session()
retry_strategy = Retry(
total=RETRY_TIMES,
backoff_factor=RETRY_DELAY,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
session.headers.update({
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
})
return session
def get_model_filename(model_id: str) -> str:
"""
从 model_id 生成 GGUF 文件名。
mradermacher/Qwen3-8B-xxx-i1-GGUF
-> Qwen3-8B-xxx.i1-Q4_0.gguf
QuantFactory/Apollo2-9B-GGUF
-> Apollo2-9B.Q8_0.gguf
"""
base_name = model_id.split("/")[-1]
if "_-_" in base_name:
base_name = base_name.split("_-_")[-1]
if base_name.lower().endswith("-gguf"):
base_name = base_name[:-5]
match = re.search(r"-i(\d+)$", base_name)
if match:
number = match.group(1)
return base_name[:match.start()] + f".i{number}-Q4_0.gguf"
return base_name + ".Q8_0.gguf"
# ============================================================
# 5. Step 1从 hf-mirror 获取组织模型
# ============================================================
def get_org_models(session: requests.Session, org_name: str) -> List[str]:
models: List[str] = []
page = START_PAGE
pages_seen = 0
while END_PAGE == 0 or page <= END_PAGE:
if MAX_SCAN_PAGES and pages_seen >= MAX_SCAN_PAGES:
break
try:
interruptible_sleep(random.uniform(*RANDOM_DELAY_RANGE))
params = {
"author": org_name,
"page": page,
"perPage": PER_PAGE,
"sort": "lastModified",
"direction": "-1",
}
response = session.get(HF_API_URL, params=params, timeout=20)
response.raise_for_status()
data = response.json()
if not data:
print(f"{page} 页无数据,停止获取")
break
page_models = []
for item in data:
model_id = item.get("modelId")
if model_id:
page_models.append(model_id)
models.append(model_id)
print(f"成功获取第 {page} 页,共 {len(page_models)} 个模型")
pages_seen += 1
page += 1
except ShutdownRequested:
raise
except Exception as e:
print(f"获取第 {page} 页失败: {e}")
pages_seen += 1
page += 1
# 去重但保持顺序
seen = set()
unique_models = []
for model in models:
if model not in seen:
unique_models.append(model)
seen.add(model)
return unique_models
# ============================================================
# 6. Step 2按目标 GGUF 文件大小筛选
# ============================================================
def extract_gguf_file_size(
session: requests.Session,
model_id: str,
target_filename: str,
) -> float:
"""
返回 GB。
找不到或失败返回 -1。
"""
interruptible_sleep(REQUEST_DELAY + random.uniform(*RANDOM_DELAY_RANGE))
files_api_url = f"https://hf-mirror.com/api/models/{model_id}/tree/main"
for retry in range(RETRY_TIMES + 1):
try:
response = session.get(files_api_url, timeout=20)
response.raise_for_status()
files_data = response.json()
for file_item in files_data:
if (
file_item.get("type") == "file"
and file_item.get("path") == target_filename
):
return file_item.get("size", 0) / (1024 ** 3)
print(f"未找到文件: {model_id} -> {target_filename}")
return -1
except requests.exceptions.HTTPError as e:
status_code = getattr(e.response, "status_code", None)
if status_code == 429 and retry < RETRY_TIMES:
wait_time = RETRY_DELAY * (retry + 1)
print(
f"触发 HF 限流: {model_id},等待 {wait_time} 秒后重试 "
f"({retry + 1}/{RETRY_TIMES})"
)
interruptible_sleep(wait_time)
continue
print(f"提取文件大小失败: {model_id} -> {target_filename}: {e}")
return -1
except Exception as e:
print(f"提取文件大小失败: {model_id} -> {target_filename}: {e}")
return -1
return -1
def filter_models_by_gguf_size(
session: requests.Session,
models: List[str],
max_size_gb: float,
) -> List[str]:
filtered: List[str] = []
for index, model in enumerate(models, start=1):
target_file = get_model_filename(model)
file_size = extract_gguf_file_size(session, model, target_file)
prefix = f"[{index}/{len(models)}]"
if file_size != -1 and file_size <= max_size_gb:
filtered.append(model)
print(
f"{prefix} 符合大小条件: {model} -> {target_file} "
f"({file_size:.2f} GB)"
)
else:
size_str = f"{file_size:.2f} GB" if file_size != -1 else "未知"
print(
f"{prefix} 不符合大小条件: {model} -> {target_file} "
f"({size_str})"
)
return filtered
# ============================================================
# 7. Step 3ModelHub 查重,过滤已入库模型
# ============================================================
def check_model_in_modelhub_db(model_id: str) -> Optional[bool]:
"""
返回:
True = 已入库
False = 未入库
None = 查询异常
"""
url = BASE_URL + MODELHUB_DB_CHECK_ENDPOINT
payload = {
"current": 1,
"pageSize": 20,
"searchText": model_id,
}
try:
response = requests.post(
url,
json=payload,
headers=BASE_HEADERS,
timeout=15,
)
if response.status_code != 200:
print(f"查重 HTTP 异常: {model_id} -> {response.status_code}")
return None
resp_json = response.json()
code = resp_json.get("code")
records = resp_json.get("data", {}).get("records", [])
if code != 0:
print(
f"查重接口业务异常: {model_id} -> "
f"{resp_json.get('message', 'unknown')}"
)
return None
if len(records) == 0:
print(f"未入库: {model_id}")
return False
first_id = records[0].get("id")
print(f"已入库: {model_id},记录 ID: {first_id}")
return True
except requests.exceptions.Timeout:
print(f"查重超时: {model_id}")
return None
except requests.exceptions.RequestException as e:
print(f"查重网络错误: {model_id}: {e}")
return None
except Exception as e:
print(f"查重未知错误: {model_id}: {e}")
return None
def filter_models_not_in_db(models: List[str]) -> List[str]:
not_in_db: List[str] = []
for index, model in enumerate(models, start=1):
print(f"[{index}/{len(models)}] 查重: {model}")
result = check_model_in_modelhub_db(model)
if result is False:
not_in_db.append(model)
elif result is True:
pass
else:
# 查询异常时,默认不提交,避免重复入库。
print(f"查重异常,跳过提交: {model}")
interruptible_sleep(0.3)
return not_in_db
# ============================================================
# 8. 登录
# ============================================================
def login() -> str:
check_shutdown()
if not USER_ACCOUNT or not USER_PASSWORD:
raise RuntimeError(
"没有设置账号密码,请在脚本中填写 USER_ACCOUNT / USER_PASSWORD。"
)
payload = {
"userAccount": USER_ACCOUNT,
"userPassword": USER_PASSWORD,
}
print("正在登录 ModelHub...")
resp = requests.post(
BASE_URL + LOGIN_ENDPOINT,
headers=BASE_HEADERS,
json=payload,
timeout=20,
)
if resp.status_code != 200:
raise RuntimeError(f"HTTP 登录失败: {resp.status_code} - {resp.text}")
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"业务登录失败: {data.get('message') or data}")
token = data["data"]["token"]
print("登录成功")
return token
def get_token() -> str:
return login()
# ============================================================
# 9. Step 4提交验证任务
# ============================================================
def is_i_variant_model(model_filename: str) -> bool:
return re.search(r"\.i\d+-Q4_0\.gguf$", model_filename) is not None
def build_config_params(model_id: str) -> Tuple[str, str]:
model_filename = get_model_filename(model_id)
if FORCE_TARGET_GPU:
target_gpu = FORCE_TARGET_GPU
elif is_i_variant_model(model_filename):
target_gpu = "Ascend_910-b4"
else:
target_gpu = "Mthreads_s4000"
if target_gpu == "Ascend_910-b4":
config_params = f"""framework: llamacpp
api: chat
lang: zh
max_model_len: 4096
max_tokens: 1024
temperature: 0.7
repetition_penalty: 1.1
top_p: 0.9
sut_config:
gpu_num: 1
values:
command: ['/workspace/llama.cpp/build_ascend/bin/llama-server', '--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '16', '--n-gpu-layers', '128', '--prio', '3', '--min_p', '0.01', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '3316', '--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', '16', '--n-gpu-layers', '128', '--prio', '3', '--min_p', '0.01', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '80', '--jinja']
"""
elif target_gpu == "Mthreads_s4000":
config_params = f"""framework: llamacpp
api: completion
max_tokens: 1024
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', '2048', '--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', '2048', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off']
"""
else:
raise ValueError(f"暂不支持的 targetGpu: {target_gpu}")
return target_gpu, config_params
def is_business_success(data: Dict) -> bool:
if not isinstance(data, dict):
return False
code = data.get("code")
success = data.get("success")
if code in (0, "0", 200, "200"):
return True
if success is True:
return True
return False
def submit_adapt_task(token: str, model_id: str) -> str:
"""
返回:
SUCCESS
LIMIT
FAILED
AUTH_FAILED
"""
check_shutdown()
target_gpu, config_params = build_config_params(model_id)
headers = {
**BASE_HEADERS,
"Authorization": f"Bearer {token}",
}
payload = {
"modelAddress": model_id,
"strategyId": STRATEGY_ID,
"taskType": TASK_TYPE,
"targetGpu": target_gpu,
"framework": FRAMEWORK,
"configParams": config_params,
}
print(f"\n提交模型: {model_id}")
print(f"GPU: {target_gpu}")
print(f"模型文件名: {get_model_filename(model_id)}")
if DEBUG_PRINT_PAYLOAD:
print(json.dumps(payload, ensure_ascii=False, indent=2))
try:
resp = requests.post(
BASE_URL + ADD_ADAPT_TASK_ENDPOINT,
headers=headers,
json=payload,
timeout=20,
)
print(f"HTTP 状态码: {resp.status_code}")
if resp.status_code in (401, 403):
print(f"鉴权失败: {resp.text}")
return "AUTH_FAILED"
try:
data = resp.json()
except Exception:
data = None
if resp.status_code != 200:
print(f"提交失败HTTP 错误: {resp.status_code} - {resp.text}")
return "FAILED"
if data is None:
print(f"提交完成,但返回不是 JSON: {resp.text}")
return "SUCCESS"
if is_business_success(data):
task_id = (
data.get("data", {}).get("taskId")
or data.get("data", {}).get("id")
or data.get("taskId")
or data.get("id")
)
if task_id:
print(f"提交成功Task ID: {task_id}")
else:
print(f"提交成功,返回: {json.dumps(data, ensure_ascii=False)}")
return "SUCCESS"
code = data.get("code")
message = data.get("message", "")
if str(code) == str(TASK_LIMIT_CODE):
print(f"达到任务上限,稍后重试: {json.dumps(data, ensure_ascii=False)}")
return "LIMIT"
if "数量已达上限" in message or "当前等待中或运行中" in message:
print(f"达到任务上限,稍后重试: {json.dumps(data, ensure_ascii=False)}")
return "LIMIT"
print(f"提交失败,业务返回: {json.dumps(data, ensure_ascii=False)}")
return "FAILED"
except requests.RequestException as e:
print(f"请求异常: {e}")
return "FAILED"
def submit_adapt_task_with_polling(token: str, model_id: str) -> Tuple[bool, str]:
retry_times = 0
current_token = token
while True:
result = submit_adapt_task(current_token, model_id)
if result == "SUCCESS":
return True, current_token
if result == "AUTH_FAILED":
print("尝试重新登录后重试当前模型...")
current_token = login()
interruptible_sleep(2)
continue
if result == "FAILED":
return False, current_token
if result == "LIMIT":
retry_times += 1
if MAX_LIMIT_RETRY_TIMES is not None and retry_times > MAX_LIMIT_RETRY_TIMES:
print(f"达到最大等待次数,放弃当前模型: {model_id}")
return False, current_token
print(
f"平台等待中/运行中的异步验证任务已满。"
f"等待 {POLL_INTERVAL_SECONDS} 秒后重试当前模型。"
f"模型: {model_id},等待次数: {retry_times}"
)
interruptible_sleep(POLL_INTERVAL_SECONDS)
continue
print(f"未知提交状态: {result}")
return False, current_token
def iter_org_models(session: requests.Session, org_name: str):
"""逐页获取模型,避免等待全量列表完成后才开始处理。"""
page = START_PAGE
pages_seen = 0
while END_PAGE == 0 or page <= END_PAGE:
if MAX_SCAN_PAGES and pages_seen >= MAX_SCAN_PAGES:
return
check_shutdown()
try:
interruptible_sleep(random.uniform(*RANDOM_DELAY_RANGE))
response = session.get(
HF_API_URL,
params={
"author": org_name,
"page": page,
"perPage": PER_PAGE,
"sort": "lastModified",
"direction": "-1",
},
timeout=20,
)
response.raise_for_status()
data = response.json()
if not data:
return
page_models = [item.get("modelId") for item in data if item.get("modelId")]
print(f"扫描第 {page} 页,共 {len(page_models)} 个模型")
for model_id in page_models:
yield model_id
pages_seen += 1
page += 1
except ShutdownRequested:
raise
except Exception as exc:
print(f"扫描第 {page} 页失败: {exc}")
pages_seen += 1
page += 1
def continuous_main() -> None:
"""持续扫描新模型,并由独立消费者逐个提交适配任务。"""
ensure_output_dir()
pending: "queue.Queue[str]" = queue.Queue()
submitted = load_model_set(SUBMITTED_FILE)
failed = load_model_set(FAILED_FILE)
scheduled: Set[str] = set()
stats = {"scanned": 0, "queued": 0, "submitted": 0, "failed": 0}
stats_lock = threading.Lock()
def submit_worker() -> None:
token: Optional[str] = None
while not stop_event_is_set():
try:
model_id = pending.get(timeout=1)
except queue.Empty:
continue
try:
if token is None:
token = get_token()
ok, token = submit_adapt_task_with_polling(token, model_id)
if ok:
submitted.add(model_id)
append_model(SUBMITTED_FILE, model_id)
with stats_lock:
stats["submitted"] += 1
else:
failed.add(model_id)
append_model(FAILED_FILE, model_id)
with stats_lock:
stats["failed"] += 1
except ShutdownRequested:
return
except Exception as exc:
print(f"提交线程异常 {model_id}: {exc}")
failed.add(model_id)
append_model(FAILED_FILE, model_id)
with stats_lock:
stats["failed"] += 1
finally:
scheduled.discard(model_id)
pending.task_done()
worker = threading.Thread(target=submit_worker, name="modelhub-submitter", daemon=True)
worker.start()
try:
while not stop_event_is_set():
session = create_hf_session()
try:
for model_id in iter_org_models(session, ORG_NAME):
check_shutdown()
with stats_lock:
stats["scanned"] += 1
if model_id in submitted or model_id in scheduled:
continue
if model_id in failed and not RETRY_FAILED:
continue
target_filename = get_model_filename(model_id)
size_gb = extract_gguf_file_size(session, model_id, target_filename)
if size_gb < 0 or size_gb > MAX_FILE_SIZE_GB:
continue
in_db = check_model_in_modelhub_db(model_id)
if in_db is not False:
continue
scheduled.add(model_id)
pending.put(model_id)
with stats_lock:
stats["queued"] += 1
print(f"已加入提交队列: {model_id} (队列长度 {pending.qsize()})")
finally:
session.close()
print(f"扫描周期完成: {stats}; 下一次扫描等待 {SCAN_INTERVAL_SECONDS}")
interruptible_sleep(SCAN_INTERVAL_SECONDS)
except ShutdownRequested:
pass
finally:
request_shutdown()
worker.join(timeout=SHUTDOWN_TIMEOUT_SECONDS)
def stop_event_is_set() -> bool:
return _shutdown_event.is_set()
# ============================================================
# 10. 主流程:筛选 -> 查重 -> 提交
# ============================================================
def main() -> None:
if PIPELINE_MODE not in {"continuous", "batch"}:
raise ValueError("PIPELINE_MODE 必须是 continuous 或 batch")
if PIPELINE_MODE == "continuous":
continuous_main()
return
ensure_output_dir()
args = parse_args()
check_shutdown()
if args.reset_submitted:
remove_file_if_exists(SUBMITTED_FILE)
if args.reset_failed:
remove_file_if_exists(FAILED_FILE)
print("=" * 80)
print("Auto HF -> ModelHub Pipeline")
print("=" * 80)
print(f"组织: {ORG_NAME}")
print(f"页码范围: {START_PAGE} - {END_PAGE}")
print(f"每页数量: {PER_PAGE}")
print(f"最大 GGUF 文件大小: {MAX_FILE_SIZE_GB} GB")
print(f"输出目录: {OUTPUT_DIR}")
print(f"运行模式: {args.run_mode}")
print(f"起始步骤: Step {args.start_step}")
print(f"强制重跑步骤: {sorted(args.force_rerun_steps) if args.force_rerun_steps else ''}")
print(f"断点续跑跳过已提交模型: {RESUME}")
print("=" * 80)
hf_session: Optional[requests.Session] = None
try:
# Step 1: 拉取 HF 模型
print("\nStep 1/4: 获取 HF / hf-mirror 模型列表")
if should_run_step(1, ALL_MODELS_FILE, args):
hf_session = hf_session or create_hf_session()
all_models = get_org_models(hf_session, ORG_NAME)
write_model_list(ALL_MODELS_FILE, all_models)
print(f"共获取到 {len(all_models)} 个模型")
print(f"已保存: {ALL_MODELS_FILE}")
else:
all_models = load_model_list(ALL_MODELS_FILE)
print_step_loaded(1, ALL_MODELS_FILE, all_models)
if not all_models:
print("没有获取到模型,结束")
return
# Step 2: 按文件大小筛选
print("\nStep 2/4: 按 GGUF 文件大小筛选")
if should_run_step(2, SIZE_FILTERED_FILE, args):
hf_session = hf_session or create_hf_session()
size_filtered_models = filter_models_by_gguf_size(
hf_session,
all_models,
MAX_FILE_SIZE_GB,
)
write_model_list(SIZE_FILTERED_FILE, size_filtered_models)
print(f"大小筛选后剩余 {len(size_filtered_models)} 个模型")
print(f"已保存: {SIZE_FILTERED_FILE}")
else:
size_filtered_models = load_model_list(SIZE_FILTERED_FILE)
print_step_loaded(2, SIZE_FILTERED_FILE, size_filtered_models)
if not size_filtered_models:
print("没有符合大小条件的模型,结束")
return
# Step 3: ModelHub 查重
print("\nStep 3/4: 查询 ModelHub 是否已入库")
if should_run_step(3, NOT_IN_DB_FILE, args):
not_in_db_models = filter_models_not_in_db(size_filtered_models)
write_model_list(NOT_IN_DB_FILE, not_in_db_models)
print(f"未入库模型共 {len(not_in_db_models)}")
print(f"已保存: {NOT_IN_DB_FILE}")
else:
not_in_db_models = load_model_list(NOT_IN_DB_FILE)
print_step_loaded(3, NOT_IN_DB_FILE, not_in_db_models)
if not not_in_db_models:
print("没有需要提交的未入库模型,结束")
return
# Step 4: 登录并提交
print("\nStep 4/4: 登录并提交验证任务")
token = get_token()
already_submitted = load_model_set(SUBMITTED_FILE) if RESUME else set()
already_failed = load_model_set(FAILED_FILE) if (RESUME and args.skip_failed) else set()
skip_models = set(already_submitted) | set(already_failed)
submit_candidates = [
model for model in not_in_db_models
if model not in skip_models
]
if RESUME:
print(f"断点续跑开启,已成功提交过 {len(already_submitted)} 个模型")
if args.skip_failed:
print(f"本次会额外跳过历史失败模型 {len(already_failed)}")
print(f"本次待提交 {len(submit_candidates)} 个模型")
success_models: List[str] = []
failed_models: List[str] = []
for index, model_id in enumerate(submit_candidates, start=1):
print("\n" + "=" * 80)
print(f"[{index}/{len(submit_candidates)}] 准备提交: {model_id}")
ok, token = submit_adapt_task_with_polling(token, model_id)
if ok:
success_models.append(model_id)
append_model(SUBMITTED_FILE, model_id)
else:
failed_models.append(model_id)
append_model(FAILED_FILE, model_id)
if index < len(submit_candidates):
interruptible_sleep(SUBMIT_INTERVAL_SECONDS)
summary = {
"org_name": ORG_NAME,
"start_page": START_PAGE,
"end_page": END_PAGE,
"max_file_size_gb": MAX_FILE_SIZE_GB,
"run_mode": args.run_mode,
"start_step": args.start_step,
"force_rerun_steps": sorted(args.force_rerun_steps),
"all_models_count": len(all_models),
"size_filtered_count": len(size_filtered_models),
"not_in_db_count": len(not_in_db_models),
"already_submitted_count": len(already_submitted),
"submitted_this_run_count": len(success_models),
"failed_this_run_count": len(failed_models),
"success_models": success_models,
"failed_models": failed_models,
}
with open(SUMMARY_FILE, "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print("\n" + "=" * 80)
print("流程完成")
print(f"HF 获取模型数: {len(all_models)}")
print(f"大小筛选后: {len(size_filtered_models)}")
print(f"未入库模型数: {len(not_in_db_models)}")
print(f"历史已提交跳过: {len(already_submitted)}")
print(f"本次提交成功: {len(success_models)}")
print(f"本次提交失败: {len(failed_models)}")
print(f"汇总文件: {SUMMARY_FILE}")
if failed_models:
print("\n失败模型:")
for model in failed_models:
print(model)
finally:
if hf_session is not None:
hf_session.close()
if __name__ == "__main__":
main()