2 Commits
v1.0.0 ... main

Author SHA1 Message Date
Codex
990be72c54 Add continuous model discovery and queued submission 2026-08-19 12:36:14 +08:00
Codex
71ea49b4c9 Vendor Python dependencies for offline image builds 2026-07-27 19:22:06 +08:00
11 changed files with 176 additions and 8 deletions

View File

@@ -1,7 +1,14 @@
STRATEGY_ID=replace-with-platform-strategy-id STRATEGY_ID=replace-with-platform-strategy-id
PIPELINE_MODE=continuous
PORT=8080 PORT=8080
PIPELINE_RUN_MODE=resume PIPELINE_RUN_MODE=resume
PIPELINE_START_STEP=1 PIPELINE_START_STEP=1
PIPELINE_FORCE_RERUN_STEPS= PIPELINE_FORCE_RERUN_STEPS=
PIPELINE_RUN_INTERVAL_SECONDS=3600 PIPELINE_RUN_INTERVAL_SECONDS=3600
PIPELINE_RETRY_INTERVAL_SECONDS=60 PIPELINE_RETRY_INTERVAL_SECONDS=60
PIPELINE_SCAN_INTERVAL_SECONDS=1800
HF_START_PAGE=1
HF_END_PAGE=0
HF_MAX_PAGES=0
HF_PER_PAGE=100
PIPELINE_RETRY_FAILED=true

View File

@@ -7,7 +7,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt COPY vendor/ ./vendor/
RUN pip install --no-cache-dir --no-index --find-links=/app/vendor -r requirements.txt
COPY . . COPY . .
RUN mkdir -p /app/pipeline_outputs RUN mkdir -p /app/pipeline_outputs

View File

@@ -1,5 +1,7 @@
# new-pipeline # new-pipeline
当前版本:`v1.1.0`
将 Hugging Face 镜像中的 GGUF 模型筛选、查重后,提交到 ModelHub 将 Hugging Face 镜像中的 GGUF 模型筛选、查重后,提交到 ModelHub
执行适配验证。项目已按 ModelHub 智能体策略运行规范进行容器化。 执行适配验证。项目已按 ModelHub 智能体策略运行规范进行容器化。
@@ -9,12 +11,20 @@
- `STRATEGY_ID`:当前策略 ID提交任务时写入 `strategyId` - `STRATEGY_ID`:当前策略 ID提交任务时写入 `strategyId`
默认模式会持续扫描所选用户的全部模型。发现符合条件且未入库的模型后立即加入提交队列;提交端遇到平台任务上限时等待,扫描端继续工作。
ModelHub 登录账号和密码按照当前部署约定固定在 ModelHub 登录账号和密码按照当前部署约定固定在
`modelhub_pipeline_resumable.py` 中,不需要额外配置环境变量。 `modelhub_pipeline_resumable.py` 中,不需要额外配置环境变量。
可选变量: 可选变量:
- `PORT`:健康检查端口,默认 `8080` - `PORT`:健康检查端口,默认 `8080`
- `PIPELINE_MODE``continuous`(默认,持续扫描)或 `batch`(兼容旧的四步批处理)。
- `PIPELINE_SCAN_INTERVAL_SECONDS`:持续模式扫描间隔,默认 `1800` 秒。
- `HF_START_PAGE` / `HF_END_PAGE`:扫描页范围;结束页为 `0` 时持续翻页直到没有数据。
- `HF_MAX_PAGES`:单次扫描最大页数,默认 `0` 表示不限制。
- `HF_PER_PAGE`:每页模型数,默认 `100`
- `PIPELINE_RETRY_FAILED`:是否重试历史失败模型,默认 `true`
- `PIPELINE_RUN_MODE``resume``fresh`,默认 `resume` - `PIPELINE_RUN_MODE``resume``fresh`,默认 `resume`
- `PIPELINE_START_STEP`:从第 14 步中的哪一步开始,默认 `1` - `PIPELINE_START_STEP`:从第 14 步中的哪一步开始,默认 `1`
- `PIPELINE_FORCE_RERUN_STEPS`:强制重跑的步骤,例如 `1,2,3` - `PIPELINE_FORCE_RERUN_STEPS`:强制重跑的步骤,例如 `1,2,3`
@@ -50,6 +60,9 @@ docker run --rm -p 8080:8080 \
镜像监听 `8080` 端口,并处理 `SIGTERM`。收到停机信号后,后台等待和轮询 镜像监听 `8080` 端口,并处理 `SIGTERM`。收到停机信号后,后台等待和轮询
会立即停止;进行中的 HTTP 请求最长等待 20 秒,随后关闭会话并退出。 会立即停止;进行中的 HTTP 请求最长等待 20 秒,随后关闭会话并退出。
`vendor/` 保存了 Linux x86_64 / Python 3.11 的离线 wheel。镜像构建不会访问
PyPI避免平台构建环境网络较慢时发生依赖下载超时。
运行时产生的断点文件位于 `/app/pipeline_outputs`。如需跨 Pod 保留断点, 运行时产生的断点文件位于 `/app/pipeline_outputs`。如需跨 Pod 保留断点,
应为该目录挂载持久卷。 应为该目录挂载持久卷。

View File

@@ -11,6 +11,7 @@ import modelhub_pipeline_resumable as pipeline
HOST = "0.0.0.0" HOST = "0.0.0.0"
VERSION = "1.1.0"
PORT = int(os.getenv("PORT", "8080")) PORT = int(os.getenv("PORT", "8080"))
RUN_INTERVAL_SECONDS = int(os.getenv("PIPELINE_RUN_INTERVAL_SECONDS", "3600")) RUN_INTERVAL_SECONDS = int(os.getenv("PIPELINE_RUN_INTERVAL_SECONDS", "3600"))
RETRY_INTERVAL_SECONDS = int(os.getenv("PIPELINE_RETRY_INTERVAL_SECONDS", "60")) RETRY_INTERVAL_SECONDS = int(os.getenv("PIPELINE_RETRY_INTERVAL_SECONDS", "60"))
@@ -67,6 +68,7 @@ class Handler(BaseHTTPRequestHandler):
200, 200,
{ {
"name": "new-pipeline", "name": "new-pipeline",
"version": VERSION,
"status": "running", "status": "running",
"strategy_id": STRATEGY_ID, "strategy_id": STRATEGY_ID,
"pipeline_status": state["pipeline_status"], "pipeline_status": state["pipeline_status"],

View File

@@ -1,9 +1,10 @@
import os
import argparse import argparse
import os
import re import re
import json import json
import random import random
import threading import threading
import queue
from typing import Dict, Tuple, List, Optional, Set from typing import Dict, Tuple, List, Optional, Set
import requests import requests
@@ -42,9 +43,9 @@ STRATEGY_ID = os.environ["STRATEGY_ID"]
ORG_NAME = "mradermacher" ORG_NAME = "mradermacher"
HF_API_URL = "https://hf-mirror.com/api/models" HF_API_URL = "https://hf-mirror.com/api/models"
PER_PAGE = 100 PER_PAGE = int(os.getenv("HF_PER_PAGE", "100"))
START_PAGE = 6 # 起始页码 START_PAGE = int(os.getenv("HF_START_PAGE", "1"))
END_PAGE = 6 # 结束页码 END_PAGE = int(os.getenv("HF_END_PAGE", "0")) # 0 表示持续翻页直到末页
REQUEST_DELAY = 1 # 每次请求的基础延迟(秒),可根据情况调整 REQUEST_DELAY = 1 # 每次请求的基础延迟(秒),可根据情况调整
RANDOM_DELAY_RANGE = (0.5, 2) # 随机延迟范围(秒),避免固定间隔被识别 RANDOM_DELAY_RANGE = (0.5, 2) # 随机延迟范围(秒),避免固定间隔被识别
RETRY_TIMES = 3 # 429错误重试次数 RETRY_TIMES = 3 # 429错误重试次数
@@ -66,6 +67,14 @@ TASK_LIMIT_CODE = 60007
POLL_INTERVAL_SECONDS = 120 POLL_INTERVAL_SECONDS = 120
MAX_LIMIT_RETRY_TIMES = None # None 表示一直等到有空位 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 DEBUG_PRINT_PAYLOAD = False
# 是否开启断点续跑:已经成功提交过的模型,下次运行自动跳过 # 是否开启断点续跑:已经成功提交过的模型,下次运行自动跳过
@@ -349,7 +358,11 @@ def get_model_filename(model_id: str) -> str:
def get_org_models(session: requests.Session, org_name: str) -> List[str]: def get_org_models(session: requests.Session, org_name: str) -> List[str]:
models: List[str] = [] models: List[str] = []
for page in range(START_PAGE, END_PAGE + 1): 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: try:
interruptible_sleep(random.uniform(*RANDOM_DELAY_RANGE)) interruptible_sleep(random.uniform(*RANDOM_DELAY_RANGE))
@@ -377,12 +390,15 @@ def get_org_models(session: requests.Session, org_name: str) -> List[str]:
models.append(model_id) models.append(model_id)
print(f"成功获取第 {page} 页,共 {len(page_models)} 个模型") print(f"成功获取第 {page} 页,共 {len(page_models)} 个模型")
pages_seen += 1
page += 1
except ShutdownRequested: except ShutdownRequested:
raise raise
except Exception as e: except Exception as e:
print(f"获取第 {page} 页失败: {e}") print(f"获取第 {page} 页失败: {e}")
continue pages_seen += 1
page += 1
# 去重但保持顺序 # 去重但保持顺序
seen = set() seen = set()
@@ -810,11 +826,140 @@ def submit_adapt_task_with_polling(token: str, model_id: str) -> Tuple[bool, str
return False, current_token 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. 主流程:筛选 -> 查重 -> 提交 # 10. 主流程:筛选 -> 查重 -> 提交
# ============================================================ # ============================================================
def main() -> None: 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() ensure_output_dir()
args = parse_args() args = parse_args()
check_shutdown() check_shutdown()

View File

@@ -1 +1 @@
requests>=2.31.0,<3.0.0 requests==2.34.2

Binary file not shown.

BIN
vendor/idna-3.18-py3-none-any.whl vendored Normal file

Binary file not shown.

BIN
vendor/requests-2.34.2-py3-none-any.whl vendored Normal file

Binary file not shown.

BIN
vendor/urllib3-2.7.0-py3-none-any.whl vendored Normal file

Binary file not shown.