2 Commits

Author SHA1 Message Date
CoolBoy
c06169d906 Rebuild agent on original pooled runner 2026-07-10 02:14:08 +08:00
CoolBoy
3a2fa86e1e Switch agent discovery to ModelScope 2026-07-10 01:44:16 +08:00
15 changed files with 526 additions and 169 deletions

View File

@@ -15,6 +15,12 @@ modelhub_submmit_api/.ipynb_checkpoints/KEY-checkpoint.md
modelhub_submmit_api/.ipynb_checkpoints/KEYS-checkpoint.md modelhub_submmit_api/.ipynb_checkpoints/KEYS-checkpoint.md
# Runtime artifacts are regenerated by the deployed strategy. # Runtime artifacts are regenerated by the deployed strategy.
runs/
daily_runs/
poll_runs/
ledger/
outcomes/
history/
modelhub_submmit_api/runs/ modelhub_submmit_api/runs/
modelhub_submmit_api/daily_runs/ modelhub_submmit_api/daily_runs/
modelhub_submmit_api/poll_runs/ modelhub_submmit_api/poll_runs/

6
.gitignore vendored
View File

@@ -12,6 +12,12 @@ modelhub_submmit_api/.ipynb_checkpoints/KEY-checkpoint.md
modelhub_submmit_api/.ipynb_checkpoints/KEYS-checkpoint.md modelhub_submmit_api/.ipynb_checkpoints/KEYS-checkpoint.md
# Runtime artifacts. # Runtime artifacts.
runs/
daily_runs/
poll_runs/
ledger/
outcomes/
history/
modelhub_submmit_api/runs/ modelhub_submmit_api/runs/
modelhub_submmit_api/daily_runs/ modelhub_submmit_api/daily_runs/
modelhub_submmit_api/poll_runs/ modelhub_submmit_api/poll_runs/

View File

@@ -15,13 +15,13 @@ submission poller in a child process.
## Runtime Environment ## Runtime Environment
The image includes single-account ModelHub and Hugging Face token fallbacks for The image includes multi-account ModelHub and ModelScope token fallbacks for
the agent platform. Environment variables can override them without rebuilding the agent platform. Environment variables can override them without rebuilding
the image. the image.
- `MODELHUB_XC_TOKEN`, `XC_TOKEN`, or `MODELHUB_TOKEN` for ModelHub API authentication - `MODELHUB_XC_TOKEN`, `XC_TOKEN`, `XC_TOKEN2...`, or `MODELHUB_XC_TOKENS` for ModelHub API authentication
- `MODELHUB_JWT_TOKEN` or `JWT_TOKEN` can be used instead when the platform provides a JWT - `MODELHUB_JWT_TOKEN` or `JWT_TOKEN` can be used instead when the platform provides a JWT
- `HF_TOKEN` optional override for the embedded Hugging Face fallback token - `MODELSCOPE_API_TOKEN` or `MODELSCOPE_TOKEN` optional override for the embedded ModelScope fallback token
- `STRATEGY_ID` is expected to be injected by the ModelHub agent platform and is attached to submissions for strategy attribution; it is not an API authentication token - `STRATEGY_ID` is expected to be injected by the ModelHub agent platform and is attached to submissions for strategy attribution; it is not an API authentication token
Optional tuning: Optional tuning:
@@ -40,6 +40,6 @@ Optional tuning:
Create a tag and submit the repository URL plus tag in "我的适配智能体". Create a tag and submit the repository URL plus tag in "我的适配智能体".
```bash ```bash
git tag agent-v1 git tag agent-v6
git push origin agent-v1 git push origin agent-v6
``` ```

View File

@@ -9,7 +9,7 @@ import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from modelhub_submmit_api.defaults import EMBEDDED_HF_TOKEN, EMBEDDED_MODELHUB_XC_TOKEN from modelhub_submmit_api.defaults import EMBEDDED_MODELHUB_XC_TOKEN, EMBEDDED_MODELSCOPE_TOKEN
HOST = "0.0.0.0" HOST = "0.0.0.0"
@@ -75,7 +75,7 @@ def _worker_command() -> list[str]:
def _config() -> dict[str, object]: def _config() -> dict[str, object]:
return { return {
"strategy_id_present": bool(os.getenv("STRATEGY_ID")), "strategy_id_present": bool(os.getenv("STRATEGY_ID")),
"hf_token_present": bool(os.getenv("HF_TOKEN") or EMBEDDED_HF_TOKEN), "modelscope_token_present": bool(os.getenv("MODELSCOPE_API_TOKEN") or os.getenv("MODELSCOPE_TOKEN") or EMBEDDED_MODELSCOPE_TOKEN),
"modelhub_auth_present": _has_modelhub_auth(), "modelhub_auth_present": _has_modelhub_auth(),
"config_error": config_error, "config_error": config_error,
"worker_running": worker is not None and worker.poll() is None, "worker_running": worker is not None and worker.poll() is None,

View File

@@ -1,12 +1,12 @@
# ModelHub Submission Runner # ModelHub Submission Runner
This package automates Hugging Face model discovery and ModelHub submission. This package automates ModelScope model discovery and ModelHub submission.
It currently supports: It currently supports:
- one-shot submission planning via `main.py` - one-shot submission planning via `main.py`
- daily batch execution via `run_daily.sh` - daily batch execution via `run_daily.sh`
- continuous queue refill via `run_poll.sh` - continuous queue refill via `run_poll.sh`
- a single ModelHub token read from environment variables or `KEY.md` - multiple ModelHub tokens read from `KEY.md` and `KEYS.md`
- automatic task/framework/template selection across the supported GPU catalog - automatic task/framework/template selection across the supported GPU catalog
## Layout ## Layout
@@ -15,8 +15,8 @@ It currently supports:
- `daily_runner.py`: daily wave orchestration - `daily_runner.py`: daily wave orchestration
- `poll_runner.py`: long-running queue refiller - `poll_runner.py`: long-running queue refiller
- `runner_common.py`: shared token / key file loading - `runner_common.py`: shared token / key file loading
- `hf_discovery.py`: Hugging Face model discovery and inspection - `hf_discovery.py`: ModelScope model discovery and inspection (keeps the legacy module name)
- `modelhub_client.py`: ModelHub API client - `modelhub_client.py`: ModelHub API client and token-pool routing
- `history_stats.py`: online history aggregation, ranking, and warnings - `history_stats.py`: online history aggregation, ranking, and warnings
- `template_selector.py`: template lookup and GPU normalization - `template_selector.py`: template lookup and GPU normalization
- `task_registry.py`: task-type and framework selection rules - `task_registry.py`: task-type and framework selection rules
@@ -24,11 +24,12 @@ It currently supports:
## Key Files ## Key Files
- `KEY.md`: optional local Hugging Face and ModelHub tokens - `KEY.md`: primary ModelScope and ModelHub tokens
- `KEYS.md`: optional supplemental ModelHub tokens
- `templates/public_submit/adapt_task_templates.jsonl`: public submit templates - `templates/public_submit/adapt_task_templates.jsonl`: public submit templates
The agent platform deployment should use environment variables instead of key The runner reads both files automatically. Add more accounts by appending
files. Set `MODELHUB_XC_TOKEN` or `XC_TOKEN` for the single submitting account. `XC_TOKEN3`, `XC_TOKEN4`, and so on to `KEYS.md`.
Template lookup is also relative. The selector searches from the current working Template lookup is also relative. The selector searches from the current working
directory and the module directory. The primary project layout is: directory and the module directory. The primary project layout is:
@@ -72,7 +73,7 @@ bash run_poll.sh --dry-run
- The runner auto-discovers all safe GPU/template combinations from the public submit catalog. - The runner auto-discovers all safe GPU/template combinations from the public submit catalog.
- Each model can be submitted at most once per GPU. - Each model can be submitted at most once per GPU.
- One ModelHub account is used per deployed agent instance. - Multiple ModelHub tokens are pooled and used to route submissions to the account with available async capacity.
- History stats are online-only. The local ledger is used for local accounting, but platform history is only used after the local ledger reaches the configured threshold. - History stats are online-only. The local ledger is used for local accounting, but platform history is only used after the local ledger reaches the configured threshold.
- The default history threshold is `500` records. - The default history threshold is `500` records.
@@ -81,7 +82,7 @@ bash run_poll.sh --dry-run
Common flags: Common flags:
- `--daily-target`: total target submissions for the day; `0` means unlimited - `--daily-target`: total target submissions for the day; `0` means unlimited
- `--min-downloads`: Hugging Face download floor - `--min-downloads`: ModelScope download floor
- `--history-stats-threshold`: local ledger threshold before using online history stats - `--history-stats-threshold`: local ledger threshold before using online history stats
- `--max-scan-models`: hard cap on scanned HF models for a run (0 = auto) - `--max-scan-models`: hard cap on scanned HF models for a run (0 = auto)
- `--scan-multiplier`: multiplier used for auto scan cap derivation from quota/queue capacity - `--scan-multiplier`: multiplier used for auto scan cap derivation from quota/queue capacity
@@ -95,7 +96,7 @@ Common flags:
`run_poll.sh` adds: `run_poll.sh` adds:
- `--poll-interval-seconds`: sleep when the account has no available slots - `--poll-interval-seconds`: sleep when all accounts are saturated
- `--idle-interval-seconds`: sleep when a cycle submits nothing - `--idle-interval-seconds`: sleep when a cycle submits nothing
- `--max-scan-models`: hard cap on scanned HF models for this cycle (0 = auto) - `--max-scan-models`: hard cap on scanned HF models for this cycle (0 = auto)
- `--scan-multiplier`: multiplier used for auto scan cap derivation from quota/queue capacity - `--scan-multiplier`: multiplier used for auto scan cap derivation from quota/queue capacity

View File

@@ -9,7 +9,7 @@ from typing import Any, Callable
from common import utc_now, write_json from common import utc_now, write_json
from hf_discovery import HuggingFaceDiscovery from hf_discovery import HuggingFaceDiscovery
from main import DEFAULT_LEDGER_PATH, DEFAULT_RUNS_DIR, run_submission from main import DEFAULT_LEDGER_PATH, DEFAULT_RUNS_DIR, run_submission
from modelhub_client import ModelHubClient from modelhub_client import ModelHubClient, ModelHubClientPool
from outcome_tracker import OutcomeTracker from outcome_tracker import OutcomeTracker
from runner_common import DEFAULT_KEY_PATH, ensure_tokens from runner_common import DEFAULT_KEY_PATH, ensure_tokens
from template_selector import TemplateSelector from template_selector import TemplateSelector
@@ -55,7 +55,7 @@ def build_parser() -> argparse.ArgumentParser:
default=4, default=4,
help="Multiplier used when auto-deriving scan limit from quota/queue capacity", help="Multiplier used when auto-deriving scan limit from quota/queue capacity",
) )
parser.add_argument("--min-downloads", type=int, default=50, help="Minimum Hugging Face download threshold") parser.add_argument("--min-downloads", type=int, default=50, help="Minimum ModelScope download threshold")
parser.add_argument( parser.add_argument(
"--history-stats-threshold", "--history-stats-threshold",
type=int, type=int,
@@ -78,16 +78,17 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning") parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning")
parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run") parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run")
parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks") parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks")
parser.add_argument("--key-path", default=str(DEFAULT_KEY_PATH), help="Path to KEY.md containing HF_TOKEN/XC_TOKEN") parser.add_argument("--key-path", default=str(DEFAULT_KEY_PATH), help="Path to KEY.md containing MODELSCOPE_TOKEN/XC_TOKEN")
parser.add_argument("--runs-dir", default=str(DEFAULT_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--runs-dir", default=str(DEFAULT_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--ledger-path", default=str(DEFAULT_LEDGER_PATH), help=argparse.SUPPRESS) parser.add_argument("--ledger-path", default=str(DEFAULT_LEDGER_PATH), help=argparse.SUPPRESS)
parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS) parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS)
parser.add_argument("--history-archive-limit", type=int, default=5000, help=argparse.SUPPRESS) parser.add_argument("--history-archive-limit", type=int, default=5000, help=argparse.SUPPRESS)
parser.add_argument("--daily-runs-dir", default=str(DEFAULT_DAILY_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--daily-runs-dir", default=str(DEFAULT_DAILY_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--hf-base-url", default=os.getenv("HF_BASE_URL", "https://huggingface.co"), help=argparse.SUPPRESS) parser.add_argument("--hf-base-url", default=os.getenv("MODELSCOPE_BASE_URL", "https://modelscope.cn"), help=argparse.SUPPRESS)
parser.add_argument("--modelhub-base-url", default=os.getenv("MODELHUB_BASE_URL", "https://modelhub.org.cn"), help=argparse.SUPPRESS) parser.add_argument("--modelhub-base-url", default=os.getenv("MODELHUB_BASE_URL", "https://modelhub.org.cn"), help=argparse.SUPPRESS)
parser.add_argument("--modelhub-token", default=os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN"), help=argparse.SUPPRESS) parser.add_argument("--modelhub-token", default=os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN"), help=argparse.SUPPRESS)
parser.add_argument("--hf-token", default=os.getenv("HF_TOKEN"), help=argparse.SUPPRESS) parser.add_argument("--hf-token", default=os.getenv("HF_TOKEN"), help=argparse.SUPPRESS)
parser.add_argument("--modelscope-token", default=os.getenv("MODELSCOPE_API_TOKEN") or os.getenv("MODELSCOPE_TOKEN"), help=argparse.SUPPRESS)
return parser return parser
@@ -138,6 +139,11 @@ def run_daily_batches(
now = now or utc_now() now = now or utc_now()
hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=base_args.hf_base_url) hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=base_args.hf_base_url)
if modelhub_client is None: if modelhub_client is None:
modelhub_tokens = list(getattr(base_args, "modelhub_tokens", None) or ([] if not base_args.modelhub_token else [base_args.modelhub_token]))
if len(modelhub_tokens) > 1:
clients = [ModelHubClient(token=token, base_url=base_args.modelhub_base_url) for token in modelhub_tokens]
modelhub_client = ModelHubClientPool(clients)
else:
modelhub_client = ModelHubClient(token=base_args.modelhub_token, base_url=base_args.modelhub_base_url) modelhub_client = ModelHubClient(token=base_args.modelhub_token, base_url=base_args.modelhub_base_url)
template_selector = template_selector or TemplateSelector() template_selector = template_selector or TemplateSelector()
@@ -284,8 +290,9 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv) args = parser.parse_args(argv)
ensure_tokens(args) ensure_tokens(args)
log( log(
f"[daily] hf_token={'set' if bool(args.hf_token) else 'missing'} " f"[daily] modelscope_token={'set' if bool(args.modelscope_token) else 'missing'} "
f"xc_token={'set' if bool(args.modelhub_token) else 'missing'} " f"xc_token={'set' if bool(args.modelhub_token) else 'missing'} "
f"xc_tokens={len(getattr(args, 'modelhub_tokens', []) or [])}"
) )
summary = run_daily_batches(base_args=args) summary = run_daily_batches(base_args=args)
print(f"daily_run_dir={summary['dailyRunDir']}") print(f"daily_run_dir={summary['dailyRunDir']}")

View File

@@ -1,2 +1,17 @@
EMBEDDED_MODELHUB_XC_TOKEN = "a14776f6e7ad4c04a1710260613c294c" EMBEDDED_MODELHUB_XC_TOKENS = [
"a14776f6e7ad4c04a1710260613c294c",
"8408eb2f73034fa09e1919406774d111",
"b0b3add66b4641d5afdc7487589905f9",
"edff358739594aa4b9f26beed6ab8799",
"d260bdd83c4b4c6faa063a16685b8908",
"fc72d989bfa84d529fff6ea33e073945",
"30d137aa08944ded8b86aac942904cc0",
"fd60e08b313048efa413f627badfe0b3",
"9bd1431410ba4558bc730481dff74d49",
"a96aea9071d84576a963c706dc71dfac",
"be09a04434fb4e379344b0554aaf6e6a",
"6032b66717e041d3adb0217e86103c6d",
]
EMBEDDED_MODELHUB_XC_TOKEN = EMBEDDED_MODELHUB_XC_TOKENS[0]
EMBEDDED_HF_TOKEN = "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" EMBEDDED_HF_TOKEN = "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"
EMBEDDED_MODELSCOPE_TOKEN = "ms-b4918c83-7eb3-4034-8635-f154938ed3f0"

View File

@@ -1,12 +1,14 @@
from __future__ import annotations from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor, as_completed
import os import os
import threading import threading
from pathlib import PurePosixPath from pathlib import PurePosixPath
from typing import Any from typing import Any
from urllib.parse import quote
from common import parse_datetime from common import parse_datetime
from defaults import EMBEDDED_MODELSCOPE_TOKEN
from http_json import HttpJsonError
from http_json import JsonHttpClient from http_json import JsonHttpClient
from models import HFModelSummary, ModelInspection from models import HFModelSummary, ModelInspection
@@ -15,20 +17,47 @@ GGUF_PRIORITY = ("q4_0.gguf", "q8_0.gguf", "fp16.gguf")
VLLM_WEIGHT_SUFFIXES = (".safetensors", ".bin", ".pth") VLLM_WEIGHT_SUFFIXES = (".safetensors", ".bin", ".pth")
ONNX_WEIGHT_SUFFIXES = (".onnx",) ONNX_WEIGHT_SUFFIXES = (".onnx",)
MODELSCOPE_TASK_TAGS = {
"text-generation": "text-generation",
"image-text-to-text": "image-text-to-text",
"visual-question-answering": "visual-question-answering",
"document-question-answering": "document-question-answering",
"video-text-to-text": "video-text-to-text",
"text-to-image": "text-to-image-synthesis",
"image-to-image": "image-to-image",
"automatic-speech-recognition": "auto-speech-recognition",
"question-answering": "question-answering",
"feature-extraction": "feature-extraction",
"sentence-similarity": "sentence-similarity",
"image-classification": "image-classification",
"zero-shot-image-classification": "zero-shot-image-classification",
"text-classification": "text-classification",
"zero-shot-classification": "zero-shot-classification",
"reinforcement-learning": "reinforcement-learning",
}
class HuggingFaceDiscovery: class HuggingFaceDiscovery:
def __init__( def __init__(
self, self,
base_url: str = "https://huggingface.co", base_url: str = "https://modelscope.cn",
http_client: JsonHttpClient | None = None, http_client: JsonHttpClient | None = None,
legacy_http_client: JsonHttpClient | None = None,
timeout: int = 30, timeout: int = 30,
retries: int = 2, retries: int = 2,
) -> None: ) -> None:
hf_token = os.getenv("HF_TOKEN") token = os.getenv("MODELSCOPE_API_TOKEN") or os.getenv("MODELSCOPE_TOKEN") or EMBEDDED_MODELSCOPE_TOKEN
headers = {"User-Agent": "modelhub-submmit-cli/0.1"} headers = {"User-Agent": "modelhub-submmit-cli/0.1"}
if hf_token: if token:
headers["Authorization"] = f"Bearer {hf_token}" headers["Authorization"] = f"Bearer {token}"
headers["Cookie"] = f"m_session_id={token}"
self.http_client = http_client or JsonHttpClient( self.http_client = http_client or JsonHttpClient(
base_url=f"{base_url.rstrip('/')}/openapi/v1",
default_headers=headers,
timeout=timeout,
retries=retries,
)
self.legacy_http_client = legacy_http_client or JsonHttpClient(
base_url=base_url, base_url=base_url,
default_headers=headers, default_headers=headers,
timeout=timeout, timeout=timeout,
@@ -50,36 +79,15 @@ class HuggingFaceDiscovery:
return [] return []
deduped: dict[str, HFModelSummary] = {} deduped: dict[str, HFModelSummary] = {}
max_workers = min(len(pipeline_tags), max(1, read_concurrency)) del read_concurrency
if max_workers <= 1: for pipeline_tag in pipeline_tags:
tag_results = [ for model in self._query_recent_models(
self._query_recent_models(
pipeline_tag=pipeline_tag, pipeline_tag=pipeline_tag,
limit=limit, limit=limit,
min_downloads=min_downloads, min_downloads=min_downloads,
updated_after=updated_after, updated_after=updated_after,
) ):
for pipeline_tag in pipeline_tags
]
else:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(
self._query_recent_models,
pipeline_tag=pipeline_tag,
limit=limit,
min_downloads=min_downloads,
updated_after=updated_after,
): pipeline_tag
for pipeline_tag in pipeline_tags
}
tag_results = []
for future in as_completed(futures):
tag_results.append(future.result())
for models in tag_results:
for model in models:
current = deduped.get(model.repo_id) current = deduped.get(model.repo_id)
if current is None or (model.last_modified or parse_datetime("1970-01-01")) > ( if current is None or (model.last_modified or parse_datetime("1970-01-01")) > (
current.last_modified or parse_datetime("1970-01-01") current.last_modified or parse_datetime("1970-01-01")
@@ -97,23 +105,42 @@ class HuggingFaceDiscovery:
min_downloads: int, min_downloads: int,
updated_after=None, updated_after=None,
) -> list[HFModelSummary]: ) -> list[HFModelSummary]:
page_size = min(max(1, limit), 100)
max_items = min(max(1, limit), 3000)
task_tag = MODELSCOPE_TASK_TAGS.get(pipeline_tag, pipeline_tag)
models: list[HFModelSummary] = []
for page_number in range(1, (max_items + page_size - 1) // page_size + 1):
try:
payload = self.http_client.request_json( payload = self.http_client.request_json(
"GET", "GET",
"/api/models", "/models",
query={ query={
"pipeline_tag": pipeline_tag, "page_number": page_number,
"sort": "lastModified", "page_size": page_size,
"direction": "-1", "sort": "last_modified",
"limit": limit, "filter.task": task_tag,
"full": "true",
}, },
) )
except HttpJsonError as exc:
print(f"[modelscope] list_models_error task={task_tag} page={page_number} error={exc}", flush=True)
break
models: list[HFModelSummary] = [] items = self._extract_models(payload)
for item in payload or []: if not items:
model = self._parse_model(item, min_downloads=min_downloads, updated_after=updated_after) break
for item in items:
model = self._parse_model(
item,
fallback_pipeline_tag=pipeline_tag,
min_downloads=min_downloads,
updated_after=updated_after,
)
if model is not None: if model is not None:
models.append(model) models.append(model)
if len(models) >= max_items:
return models
if len(items) < page_size:
break
return models return models
def list_recent_text_generation_models( def list_recent_text_generation_models(
@@ -140,40 +167,67 @@ class HuggingFaceDiscovery:
if cached is not None: if cached is not None:
return list(cached) return list(cached)
payload = self.http_client.request_json( encoded_repo_id = "/".join(quote(part, safe="") for part in repo_id.split("/"))
try:
payload = self.legacy_http_client.request_json(
"GET", "GET",
f"/api/models/{repo_id}/tree/main", f"/api/v1/models/{encoded_repo_id}/repo/files",
query={"recursive": "1"}, query={"Revision": "master", "Recursive": "true"},
) )
except HttpJsonError as exc:
print(f"[modelscope] repo_tree_error repo={repo_id} error={exc}", flush=True)
payload = None
entries: list[dict[str, Any]] entries = self._extract_files(payload)
if isinstance(payload, list):
entries = payload
elif isinstance(payload, dict):
for key in ("items", "tree", "siblings"):
value = payload.get(key)
if isinstance(value, list):
entries = value
break
else:
entries = []
else:
entries = []
with self._repo_tree_lock: with self._repo_tree_lock:
self._repo_tree_cache[repo_id] = list(entries) self._repo_tree_cache[repo_id] = list(entries)
return list(entries) return list(entries)
@staticmethod @staticmethod
def _parse_model(item: dict[str, Any], *, min_downloads: int, updated_after=None) -> HFModelSummary | None: def _extract_models(payload: Any) -> list[dict[str, Any]]:
repo_id = item.get("id") or item.get("modelId") data = payload.get("data") if isinstance(payload, dict) else payload
if isinstance(data, dict):
for key in ("models", "Models", "items", "list", "data", "results"):
value = data.get(key)
if isinstance(value, list):
return [item for item in value if isinstance(item, dict)]
if isinstance(data, list):
return [item for item in data if isinstance(item, dict)]
return []
@staticmethod
def _extract_files(payload: Any) -> list[dict[str, Any]]:
data = payload.get("Data") if isinstance(payload, dict) else payload
if isinstance(data, dict):
for key in ("Files", "files", "items", "tree"):
value = data.get(key)
if isinstance(value, list):
return [item for item in value if isinstance(item, dict)]
if isinstance(data, list):
return [item for item in data if isinstance(item, dict)]
return []
@staticmethod
def _parse_model(
item: dict[str, Any],
*,
fallback_pipeline_tag: str,
min_downloads: int,
updated_after=None,
) -> HFModelSummary | None:
repo_id = item.get("id") or item.get("model_id") or item.get("modelId")
if not repo_id:
owner = item.get("owner") or item.get("Owner") or item.get("Path")
name = item.get("name") or item.get("Name")
repo_id = f"{owner}/{name}" if owner and name else None
if not repo_id: if not repo_id:
return None return None
downloads = int(item.get("downloads") or 0) downloads = int(item.get("downloads") or item.get("Downloads") or 0)
if downloads < min_downloads: if downloads < min_downloads:
return None return None
pipeline_tag = item.get("pipeline_tag") or item.get("pipelineTag") pipeline_tag = fallback_pipeline_tag
last_modified = parse_datetime(item.get("lastModified") or item.get("last_modified")) last_modified = parse_datetime(item.get("last_modified") or item.get("UpdatedAt") or item.get("LastUpdatedTime"))
if updated_after and last_modified and last_modified < updated_after: if updated_after and last_modified and last_modified < updated_after:
return None return None
return HFModelSummary( return HFModelSummary(
@@ -181,7 +235,7 @@ class HuggingFaceDiscovery:
downloads=downloads, downloads=downloads,
last_modified=last_modified, last_modified=last_modified,
pipeline_tag=pipeline_tag, pipeline_tag=pipeline_tag,
created_at=parse_datetime(item.get("createdAt") or item.get("created_at")), created_at=parse_datetime(item.get("created_at") or item.get("CreatedAt")),
) )
@@ -192,10 +246,10 @@ def inspect_repo_tree(repo_id: str, entries: list[dict[str, Any]]) -> ModelInspe
onnx_files: list[str] = [] onnx_files: list[str] = []
for entry in entries: for entry in entries:
path = entry.get("path") or entry.get("rfilename") or entry.get("name") path = entry.get("path") or entry.get("Path") or entry.get("rfilename") or entry.get("name") or entry.get("Name")
if not path: if not path:
continue continue
entry_type = (entry.get("type") or "").lower() entry_type = (entry.get("type") or entry.get("Type") or "").lower()
if entry_type in {"directory", "dir", "folder"}: if entry_type in {"directory", "dir", "folder"}:
continue continue
file_paths.append(path) file_paths.append(path)

View File

@@ -16,7 +16,7 @@ from history_stats import (
load_ledger, load_ledger,
update_history_archive, update_history_archive,
) )
from modelhub_client import ModelHubAPIError, ModelHubClient from modelhub_client import ModelHubAPIError, ModelHubClient, ModelHubClientPool
from models import CandidateModel, HFModelSummary, ModelInspection from models import CandidateModel, HFModelSummary, ModelInspection
from outcome_tracker import DEFAULT_OUTCOMES_PATH, OutcomeTracker from outcome_tracker import DEFAULT_OUTCOMES_PATH, OutcomeTracker
from task_registry import TASK_SPEC_BY_TYPE, all_task_types, choose_framework_for_task, choose_text_generation_framework, pipeline_tags_for_task_types, task_specs_for_model from task_registry import TASK_SPEC_BY_TYPE, all_task_types, choose_framework_for_task, choose_text_generation_framework, pipeline_tags_for_task_types, task_specs_for_model
@@ -28,11 +28,11 @@ DEFAULT_LEDGER_PATH = Path("ledger/submissions.jsonl")
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Auto discover and submit public Hugging Face models to ModelHub.") parser = argparse.ArgumentParser(description="Auto discover and submit public ModelScope models to ModelHub.")
parser.add_argument("--gpu", help="Single GPU alias or platform name, for example: k100") parser.add_argument("--gpu", help="Single GPU alias or platform name, for example: k100")
parser.add_argument("--gpus", help="Comma-separated GPU aliases/platform names. Omit to auto-use all safe GPUs from templates.") parser.add_argument("--gpus", help="Comma-separated GPU aliases/platform names. Omit to auto-use all safe GPUs from templates.")
parser.add_argument("--task-types", help="Comma-separated ModelHub task types. Omit to auto-enable all supported task types.") parser.add_argument("--task-types", help="Comma-separated ModelHub task types. Omit to auto-enable all supported task types.")
parser.add_argument("--limit", type=int, default=300, help="Maximum number of Hugging Face models to scan per pipeline tag") parser.add_argument("--limit", type=int, default=300, help="Maximum number of ModelScope models to scan per pipeline tag")
parser.add_argument("--max-scan-models", type=int, default=0, help="Hard cap on total scanned models (0 means auto)") parser.add_argument("--max-scan-models", type=int, default=0, help="Hard cap on total scanned models (0 means auto)")
parser.add_argument( parser.add_argument(
"--scan-multiplier", "--scan-multiplier",
@@ -73,7 +73,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--outcomes-path", default=str(DEFAULT_OUTCOMES_PATH), help=argparse.SUPPRESS) parser.add_argument("--outcomes-path", default=str(DEFAULT_OUTCOMES_PATH), help=argparse.SUPPRESS)
parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS) parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS)
parser.add_argument("--history-archive-limit", type=int, default=5000, help=argparse.SUPPRESS) parser.add_argument("--history-archive-limit", type=int, default=5000, help=argparse.SUPPRESS)
parser.add_argument("--hf-base-url", default=os.getenv("HF_BASE_URL", "https://huggingface.co"), help=argparse.SUPPRESS) parser.add_argument("--hf-base-url", default=os.getenv("MODELSCOPE_BASE_URL", "https://modelscope.cn"), help=argparse.SUPPRESS)
parser.add_argument("--modelhub-base-url", default=os.getenv("MODELHUB_BASE_URL", "https://modelhub.org.cn"), help=argparse.SUPPRESS) parser.add_argument("--modelhub-base-url", default=os.getenv("MODELHUB_BASE_URL", "https://modelhub.org.cn"), help=argparse.SUPPRESS)
parser.add_argument("--modelhub-token", default=os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN"), help=argparse.SUPPRESS) parser.add_argument("--modelhub-token", default=os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN"), help=argparse.SUPPRESS)
return parser return parser
@@ -194,7 +194,7 @@ def choose_candidate_for_gpu(
def resolve_submit_concurrency( def resolve_submit_concurrency(
args: argparse.Namespace, args: argparse.Namespace,
*, *,
modelhub_client: ModelHubClient | Any, modelhub_client: ModelHubClient | ModelHubClientPool | Any,
planned_submit_count: int, planned_submit_count: int,
) -> int: ) -> int:
explicit_workers = int(getattr(args, "submit_concurrency", 0) or 0) explicit_workers = int(getattr(args, "submit_concurrency", 0) or 0)
@@ -315,6 +315,11 @@ def submit_candidate(
"responseData": response.get("data"), "responseData": response.get("data"),
} }
except ModelHubAPIError as exc: except ModelHubAPIError as exc:
print(
f"[submit] failed repo={candidate['repoId']} gpu={candidate['targetGpu']} "
f"framework={candidate['framework']} reason={exc}",
flush=True,
)
return { return {
"outcome": "failed", "outcome": "failed",
"candidate": candidate, "candidate": candidate,
@@ -340,7 +345,7 @@ def run_submission(
*, *,
now=None, now=None,
hf_discovery: HuggingFaceDiscovery | None = None, hf_discovery: HuggingFaceDiscovery | None = None,
modelhub_client: ModelHubClient | None = None, modelhub_client: ModelHubClient | ModelHubClientPool | None = None,
template_selector: TemplateSelector | None = None, template_selector: TemplateSelector | None = None,
outcome_tracker: OutcomeTracker | None = None, outcome_tracker: OutcomeTracker | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -352,7 +357,13 @@ def run_submission(
raise RuntimeError("No auto-submittable GPUs are available for the selected task types") raise RuntimeError("No auto-submittable GPUs are available for the selected task types")
hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=args.hf_base_url) hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=args.hf_base_url)
modelhub_client = modelhub_client or ModelHubClient(token=args.modelhub_token, base_url=args.modelhub_base_url) if modelhub_client is None:
modelhub_tokens = list(getattr(args, "modelhub_tokens", None) or ([] if not args.modelhub_token else [args.modelhub_token]))
if len(modelhub_tokens) > 1:
clients = [ModelHubClient(token=token, base_url=args.modelhub_base_url) for token in modelhub_tokens]
modelhub_client = ModelHubClientPool(clients)
else:
modelhub_client = ModelHubClient(token=args.modelhub_token, base_url=args.modelhub_base_url)
runs_dir = Path(args.runs_dir) runs_dir = Path(args.runs_dir)
ledger_path = Path(args.ledger_path) ledger_path = Path(args.ledger_path)

View File

@@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
import os import os
import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
@@ -26,21 +29,10 @@ class ModelHubClient:
retries: int = 2, retries: int = 2,
http_client: JsonHttpClient | None = None, http_client: JsonHttpClient | None = None,
) -> None: ) -> None:
self.token = ( self.token = token or os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN") or EMBEDDED_MODELHUB_XC_TOKEN
token if not self.token and http_client is None:
or os.getenv("MODELHUB_XC_TOKEN") raise ValueError("ModelHub token is required. Set MODELHUB_XC_TOKEN or XC_TOKEN.")
or os.getenv("XC_TOKEN") default_headers = {"Xc-Token": self.token} if self.token else {}
or os.getenv("MODELHUB_TOKEN")
or EMBEDDED_MODELHUB_XC_TOKEN
)
self.jwt_token = os.getenv("MODELHUB_JWT_TOKEN") or os.getenv("JWT_TOKEN")
if not self.token and not self.jwt_token and http_client is None:
raise ValueError("ModelHub token is required. Set MODELHUB_XC_TOKEN/XC_TOKEN or MODELHUB_JWT_TOKEN/JWT_TOKEN.")
default_headers = {}
if self.token:
default_headers["Xc-Token"] = self.token
if self.jwt_token:
default_headers["Authorization"] = f"Bearer {self.jwt_token}"
self.http_client = http_client or JsonHttpClient( self.http_client = http_client or JsonHttpClient(
base_url=base_url, base_url=base_url,
default_headers=default_headers, default_headers=default_headers,
@@ -239,3 +231,175 @@ def is_active_task(task: dict[str, Any]) -> bool:
if status in TERMINAL_TASK_STATUSES: if status in TERMINAL_TASK_STATUSES:
return False return False
return True return True
class ModelHubClientPool:
def __init__(self, clients: list[ModelHubClient], *, active_task_cap: int = 100) -> None:
if not clients:
raise ValueError("At least one ModelHub client is required")
self.clients = clients
self.active_task_cap = active_task_cap
self._active_counts: list[int] = [0 for _ in clients]
self._active_refresh_at: float = 0.0
self._active_counts_ttl: float = 30.0
self._state_lock = threading.Lock()
# Per-cycle cache for search_by_model_id results (model_id -> merged verify result map)
self._verify_cache: dict[str, dict[str, Any]] = {}
# Single reader client to avoid fanout on read operations
self._reader = clients[0]
def _safe_count_active_tasks(self, client: ModelHubClient) -> int:
try:
return client.count_active_tasks(max_count=self.active_task_cap, page_size=200)
except Exception:
return self.active_task_cap
def _safe_search_by_model_id(self, client: ModelHubClient, model_id: str) -> dict[str, Any]:
try:
payload = client.search_by_model_id(model_id)
return payload if isinstance(payload, dict) else {}
except Exception:
return {}
def _safe_list_tasks(self, client: ModelHubClient, kwargs: dict[str, Any]) -> list[dict[str, Any]]:
try:
return client.list_tasks(**kwargs)
except Exception:
return []
def _refresh_active_counts(self, *, force: bool = False) -> None:
now = time.time()
if (not force) and self._active_counts and (now - self._active_refresh_at) < self._active_counts_ttl:
return
def _to_indexed_result(index: int, client: ModelHubClient) -> tuple[int, int]:
return index, self._safe_count_active_tasks(client)
results: list[tuple[int, int]] = []
with ThreadPoolExecutor(max_workers=min(len(self.clients), 12)) as executor:
futures = {executor.submit(_to_indexed_result, index, client): index for index, client in enumerate(self.clients)}
for future in as_completed(futures):
index = futures[future]
try:
results.append(future.result())
except Exception:
results.append((index, self.active_task_cap))
self._active_counts = [0 for _ in self.clients]
for index, count in sorted(results, key=lambda item: item[0]):
self._active_counts[index] = count
# Use current time after refresh completes, not the stale 'now' from function start
self._active_refresh_at = time.time()
def active_task_counts(self) -> list[int]:
with self._state_lock:
now = time.time()
if self._active_counts and (now - self._active_refresh_at) < self._active_counts_ttl:
pass # use cached counts
else:
self._refresh_active_counts()
return list(self._active_counts)
def available_submit_slots(self) -> int:
with self._state_lock:
# Only refresh if cache is stale (respects TTL) or counts are empty
now = time.time()
if self._active_counts and (now - self._active_refresh_at) < self._active_counts_ttl:
pass # use cached counts
else:
self._refresh_active_counts()
return sum(max(0, self.active_task_cap - count) for count in self._active_counts)
def list_tasks(self, **kwargs): # noqa: ANN003, ANN001
# Default: use only the reader client to avoid fanout amplification.
# The fanout_all parameter allows explicit cross-account merging when needed.
fanout_all = kwargs.pop("_fanout_all", False)
if not fanout_all:
return self._safe_list_tasks(self._reader, kwargs)
merged: list[dict[str, Any]] = []
seen: set[str] = set()
with ThreadPoolExecutor(max_workers=min(len(self.clients), 12)) as executor:
futures = {executor.submit(self._safe_list_tasks, client, kwargs): index for index, client in enumerate(self.clients)}
for future in as_completed(futures):
try:
tasks = future.result()
except Exception:
tasks = []
for task in tasks:
task_id = str(task.get("taskId")) if task.get("taskId") is not None else None
if task_id and task_id in seen:
continue
if task_id:
seen.add(task_id)
merged.append(task)
return merged
def _read_from_cache(self, model_id: str) -> dict[str, Any] | None:
cached = self._verify_cache.get(model_id)
if cached is None:
return None
return dict(cached)
def _write_to_cache(self, model_id: str, payload: dict[str, Any]) -> None:
self._verify_cache[model_id] = payload
def search_by_model_id(self, model_id: str) -> dict[str, Any]:
# Check cache first (per-cycle cache to avoid repeated API calls for the same model)
with self._state_lock:
cached = self._read_from_cache(model_id)
if cached is not None:
return cached
# Only query ONE client (the reader) instead of fanning out to all clients.
# verifyResult is model-specific platform data, not account-specific.
response = self._safe_search_by_model_id(self._reader, model_id)
if not isinstance(response, dict):
response = {"code": 0, "data": {"verifyResult": {}}}
with self._state_lock:
self._write_to_cache(model_id, response)
return response
def get_verify_result_map(self, model_id: str) -> dict[str, Any]:
payload = self.search_by_model_id(model_id)
return ((payload.get("data") or {}).get("verifyResult") or {})
def processed_gpus_for_model(self, model_id: str) -> set[str]:
return set(self.get_verify_result_map(model_id).keys())
def add_task(self, payload: dict[str, Any]) -> dict[str, Any]:
with self._state_lock:
self._refresh_active_counts(force=True)
selected_index = None
best_remaining = -1
for index, active_count in enumerate(self._active_counts):
remaining = self.active_task_cap - active_count
if remaining > best_remaining:
best_remaining = remaining
selected_index = index
if selected_index is None or best_remaining <= 0:
raise ModelHubAPIError(
f"当前等待中或运行中的异步模型验证任务数量已达上限({self.active_task_cap}"
)
self._active_counts[selected_index] += 1
selected_client = self.clients[selected_index]
try:
response = selected_client.add_task(payload)
except Exception:
with self._state_lock:
self._active_counts[selected_index] -= 1
raise
return response
def list_tasks_page(self, **kwargs): # noqa: ANN003, ANN001
"""Single-page task listing via the reader client (no fanout)."""
return self._reader.list_tasks_page(**kwargs)
def find_recent_task_id(self, model_id: str, gpu_type: str, submitted_after: datetime) -> str | None:
for client in self.clients:
task_id = client.find_recent_task_id(model_id, gpu_type, submitted_after)
if task_id is not None:
return task_id
return None

View File

@@ -26,7 +26,7 @@ class HFModelSummary:
@property @property
def model_address(self) -> str: def model_address(self) -> str:
return f"https://huggingface.co/{self.repo_id}" return f"https://modelscope.cn/models/{self.repo_id}"
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -7,7 +7,7 @@ from typing import Any
from common import append_jsonl, parse_datetime, read_jsonl, utc_now, write_jsonl from common import append_jsonl, parse_datetime, read_jsonl, utc_now, write_jsonl
from history_stats import classify_failure, is_failure, is_success from history_stats import classify_failure, is_failure, is_success
from modelhub_client import ModelHubClient from modelhub_client import ModelHubClient, ModelHubClientPool
DEFAULT_OUTCOMES_PATH = Path("outcomes/submissions.jsonl") DEFAULT_OUTCOMES_PATH = Path("outcomes/submissions.jsonl")
@@ -72,11 +72,14 @@ class OutcomeTracker:
self._by_model_gpu[(model_id, target_gpu)].append(record) self._by_model_gpu[(model_id, target_gpu)].append(record)
append_jsonl(self.path, record) append_jsonl(self.path, record)
def sync_from_api(self, client: ModelHubClient) -> int: def sync_from_api(self, client: ModelHubClient | ModelHubClientPool) -> int:
try: try:
begin = self._last_sync_time begin = self._last_sync_time
end = utc_now() end = utc_now()
# Use fanout for pools to sync outcomes across all accounts
list_kwargs: dict[str, Any] = {"begin_time": begin, "end_time": end, "page_size": 100, "only_mine": True} list_kwargs: dict[str, Any] = {"begin_time": begin, "end_time": end, "page_size": 100, "only_mine": True}
if isinstance(client, ModelHubClientPool):
list_kwargs["_fanout_all"] = True
tasks = client.list_tasks(**list_kwargs) tasks = client.list_tasks(**list_kwargs)
except Exception: except Exception:
return 0 return 0

View File

@@ -11,7 +11,7 @@ from common import utc_now, write_json
from daily_runner import DEFAULT_DAILY_RUNS_DIR, log, run_daily_batches from daily_runner import DEFAULT_DAILY_RUNS_DIR, log, run_daily_batches
from hf_discovery import HuggingFaceDiscovery from hf_discovery import HuggingFaceDiscovery
from main import DEFAULT_LEDGER_PATH, DEFAULT_RUNS_DIR from main import DEFAULT_LEDGER_PATH, DEFAULT_RUNS_DIR
from modelhub_client import ModelHubClient from modelhub_client import ModelHubClient, ModelHubClientPool
from outcome_tracker import DEFAULT_OUTCOMES_PATH, OutcomeTracker from outcome_tracker import DEFAULT_OUTCOMES_PATH, OutcomeTracker
from runner_common import DEFAULT_KEY_PATH, ensure_tokens from runner_common import DEFAULT_KEY_PATH, ensure_tokens
from template_selector import TemplateSelector from template_selector import TemplateSelector
@@ -25,7 +25,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--daily-target", type=int, default=0, help="Total submissions to aim for per UTC day; 0 means unlimited") parser.add_argument("--daily-target", type=int, default=0, help="Total submissions to aim for per UTC day; 0 means unlimited")
parser.add_argument("--gpu", help="Single GPU alias or platform name, for example: k100") parser.add_argument("--gpu", help="Single GPU alias or platform name, for example: k100")
parser.add_argument("--gpus", help="Comma-separated GPU aliases/platform names. Omit to auto-use all safe GPUs.") parser.add_argument("--gpus", help="Comma-separated GPU aliases/platform names. Omit to auto-use all safe GPUs.")
parser.add_argument("--min-downloads", type=int, default=50, help="Minimum Hugging Face download threshold") parser.add_argument("--min-downloads", type=int, default=50, help="Minimum ModelScope download threshold")
parser.add_argument( parser.add_argument(
"--history-stats-threshold", "--history-stats-threshold",
type=int, type=int,
@@ -55,7 +55,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning") parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning")
parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run") parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run")
parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks") parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks")
parser.add_argument("--key-path", default=str(DEFAULT_KEY_PATH), help="Path to KEY.md containing HF_TOKEN/XC_TOKEN") parser.add_argument("--key-path", default=str(DEFAULT_KEY_PATH), help="Path to KEY.md containing MODELSCOPE_TOKEN/XC_TOKEN")
parser.add_argument("--runs-dir", default=str(DEFAULT_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--runs-dir", default=str(DEFAULT_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--ledger-path", default=str(DEFAULT_LEDGER_PATH), help=argparse.SUPPRESS) parser.add_argument("--ledger-path", default=str(DEFAULT_LEDGER_PATH), help=argparse.SUPPRESS)
parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS) parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS)
@@ -63,10 +63,11 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--daily-runs-dir", default=str(DEFAULT_DAILY_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--daily-runs-dir", default=str(DEFAULT_DAILY_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--poll-runs-dir", default=str(DEFAULT_POLL_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--poll-runs-dir", default=str(DEFAULT_POLL_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--outcomes-path", default=str(DEFAULT_OUTCOMES_PATH), help=argparse.SUPPRESS) parser.add_argument("--outcomes-path", default=str(DEFAULT_OUTCOMES_PATH), help=argparse.SUPPRESS)
parser.add_argument("--hf-base-url", default="https://huggingface.co", help=argparse.SUPPRESS) parser.add_argument("--hf-base-url", default="https://modelscope.cn", help=argparse.SUPPRESS)
parser.add_argument("--modelhub-base-url", default="https://modelhub.org.cn", help=argparse.SUPPRESS) parser.add_argument("--modelhub-base-url", default="https://modelhub.org.cn", help=argparse.SUPPRESS)
parser.add_argument("--modelhub-token", default=None, help=argparse.SUPPRESS) parser.add_argument("--modelhub-token", default=None, help=argparse.SUPPRESS)
parser.add_argument("--hf-token", default=None, help=argparse.SUPPRESS) parser.add_argument("--hf-token", default=None, help=argparse.SUPPRESS)
parser.add_argument("--modelscope-token", default=None, help=argparse.SUPPRESS)
parser.add_argument("--poll-interval-seconds", type=int, default=60, help="Sleep between polling cycles when no slots are available") parser.add_argument("--poll-interval-seconds", type=int, default=60, help="Sleep between polling cycles when no slots are available")
parser.add_argument("--idle-interval-seconds", type=int, default=30, help="Sleep between cycles when a scan submits nothing") parser.add_argument("--idle-interval-seconds", type=int, default=30, help="Sleep between cycles when a scan submits nothing")
parser.add_argument("--post-cycle-cooldown-seconds", type=int, default=0, help="Short sleep after a successful cycle") parser.add_argument("--post-cycle-cooldown-seconds", type=int, default=0, help="Short sleep after a successful cycle")
@@ -84,7 +85,11 @@ def _make_cycle_args(base_args: argparse.Namespace) -> argparse.Namespace:
return cycle_args return cycle_args
def _build_modelhub_client(base_args: argparse.Namespace) -> ModelHubClient: def _build_modelhub_client(base_args: argparse.Namespace) -> ModelHubClient | ModelHubClientPool:
modelhub_tokens = list(getattr(base_args, "modelhub_tokens", None) or ([] if not base_args.modelhub_token else [base_args.modelhub_token]))
if len(modelhub_tokens) > 1:
clients = [ModelHubClient(token=token, base_url=base_args.modelhub_base_url) for token in modelhub_tokens]
return ModelHubClientPool(clients)
return ModelHubClient(token=base_args.modelhub_token, base_url=base_args.modelhub_base_url) return ModelHubClient(token=base_args.modelhub_token, base_url=base_args.modelhub_base_url)
@@ -94,7 +99,7 @@ def run_poll_loop(
now=None, now=None,
run_fn: Callable[..., dict[str, Any]] = run_daily_batches, run_fn: Callable[..., dict[str, Any]] = run_daily_batches,
hf_discovery: HuggingFaceDiscovery | None = None, hf_discovery: HuggingFaceDiscovery | None = None,
modelhub_client: ModelHubClient | None = None, modelhub_client: ModelHubClient | ModelHubClientPool | None = None,
template_selector: TemplateSelector | None = None, template_selector: TemplateSelector | None = None,
outcome_tracker: OutcomeTracker | None = None, outcome_tracker: OutcomeTracker | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -244,8 +249,9 @@ def main(argv: list[str] | None = None) -> int:
return 0 return 0
log( log(
f"[poll] hf_token={'set' if bool(args.hf_token) else 'missing'} " f"[poll] modelscope_token={'set' if bool(args.modelscope_token) else 'missing'} "
f"xc_token={'set' if bool(args.modelhub_token) else 'missing'} " f"xc_token={'set' if bool(args.modelhub_token) else 'missing'} "
f"xc_tokens={len(getattr(args, 'modelhub_tokens', []) or [])}"
) )
summary = run_poll_loop(base_args=args) summary = run_poll_loop(base_args=args)
print(f"poll_run_dir={summary['pollRunDir']}") print(f"poll_run_dir={summary['pollRunDir']}")

View File

@@ -4,12 +4,22 @@ import argparse
import os import os
from pathlib import Path from pathlib import Path
from defaults import EMBEDDED_HF_TOKEN, EMBEDDED_MODELHUB_XC_TOKEN from defaults import EMBEDDED_HF_TOKEN, EMBEDDED_MODELHUB_XC_TOKENS, EMBEDDED_MODELSCOPE_TOKEN
DEFAULT_KEY_PATH = Path("KEY.md") DEFAULT_KEY_PATH = Path("KEY.md")
XC_TOKEN_ENV_NAMES = ("MODELHUB_XC_TOKEN", "XC_TOKEN", "MODELHUB_TOKEN") DEFAULT_KEYS_PATH = Path("KEYS.md")
JWT_TOKEN_ENV_NAMES = ("MODELHUB_JWT_TOKEN", "JWT_TOKEN") MODULE_DIR = Path(__file__).resolve().parent
MODELSCOPE_TOKEN_ENV_NAMES = ("MODELSCOPE_API_TOKEN", "MODELSCOPE_TOKEN")
def _token_sort_key(key: str) -> tuple[int, str]:
suffix = key.removeprefix("XC_TOKEN")
if not suffix:
return (0, key)
if suffix.isdigit():
return (int(suffix), key)
return (10_000, key)
def load_key_file(path: Path) -> dict[str, str]: def load_key_file(path: Path) -> dict[str, str]:
@@ -25,42 +35,108 @@ def load_key_file(path: Path) -> dict[str, str]:
return loaded return loaded
def first_value(values: dict[str, str], names: tuple[str, ...]) -> str | None: def load_key_files(*paths: Path) -> dict[str, str]:
for name in names: loaded: dict[str, str] = {}
value = os.getenv(name) or values.get(name) for path in paths:
if value: if path.exists():
return value.strip() loaded.update(load_key_file(path))
return None return loaded
def load_modelhub_tokens(values: dict[str, str]) -> list[str]:
tokens: list[tuple[str, str]] = []
seen: set[str] = set()
for key, value in values.items():
if key == "MODELHUB_XC_TOKEN" or key.startswith("XC_TOKEN"):
token = value.strip()
if token and token not in seen:
seen.add(token)
tokens.append((key, token))
tokens.sort(key=lambda item: _token_sort_key(item[0]))
return [token for _, token in tokens]
def _split_token_list(value: str | None) -> list[str]:
if not value:
return []
tokens: list[str] = []
for raw in value.replace(",", "\n").replace(";", "\n").splitlines():
token = raw.strip()
if token:
tokens.append(token)
return tokens
def _add_token(tokens: list[str], token: str | None) -> None:
for item in _split_token_list(token):
if item and item not in tokens:
tokens.append(item)
def ensure_tokens(args: argparse.Namespace) -> None: def ensure_tokens(args: argparse.Namespace) -> None:
primary_key_path = Path(getattr(args, "key_path", DEFAULT_KEY_PATH)) primary_key_path = Path(getattr(args, "key_path", DEFAULT_KEY_PATH))
supplemental_key_path = primary_key_path.with_name(DEFAULT_KEYS_PATH.name)
if not primary_key_path.exists(): if not primary_key_path.exists():
parent_key = primary_key_path.parent.parent / primary_key_path.name for candidate in (
if parent_key.exists(): MODULE_DIR / primary_key_path.name,
primary_key_path = parent_key primary_key_path.parent.parent / primary_key_path.name,
values = load_key_file(primary_key_path) MODULE_DIR.parent / primary_key_path.name,
):
if candidate.exists():
primary_key_path = candidate
supplemental_key_path = candidate.with_name(DEFAULT_KEYS_PATH.name)
break
values = load_key_files(primary_key_path, supplemental_key_path)
if not getattr(args, "hf_token", None): if not getattr(args, "hf_token", None):
args.hf_token = os.getenv("HF_TOKEN") or values.get("HF_TOKEN") args.hf_token = values.get("HF_TOKEN") or EMBEDDED_HF_TOKEN
if not getattr(args, "hf_token", None): if not getattr(args, "modelscope_token", None):
args.hf_token = EMBEDDED_HF_TOKEN args.modelscope_token = (
os.getenv("MODELSCOPE_API_TOKEN")
or os.getenv("MODELSCOPE_TOKEN")
or values.get("MODELSCOPE_API_TOKEN")
or values.get("MODELSCOPE_TOKEN")
or EMBEDDED_MODELSCOPE_TOKEN
)
tokens = load_modelhub_tokens(values)
env_token_items: list[tuple[str, str]] = []
for key, value in os.environ.items():
if key == "MODELHUB_XC_TOKEN" or key.startswith("XC_TOKEN"):
token = value.strip()
if token:
env_token_items.append((key, token))
env_token_items.sort(key=lambda item: _token_sort_key(item[0]))
env_tokens = [token for _, token in env_token_items]
modelhub_token = getattr(args, "modelhub_token", None) modelhub_token = getattr(args, "modelhub_token", None)
if not modelhub_token: if modelhub_token and modelhub_token not in env_tokens:
modelhub_token = first_value(values, XC_TOKEN_ENV_NAMES) env_tokens.insert(0, modelhub_token)
if not modelhub_token: if not env_tokens:
modelhub_token = EMBEDDED_MODELHUB_XC_TOKEN env_tokens = tokens
jwt_token = first_value(values, JWT_TOKEN_ENV_NAMES) else:
for token in tokens:
if token not in env_tokens:
env_tokens.append(token)
for env_name in ("MODELHUB_XC_TOKENS", "XC_TOKENS", "MODELHUB_TOKENS"):
for token in _split_token_list(os.getenv(env_name)):
_add_token(env_tokens, token)
for token in EMBEDDED_MODELHUB_XC_TOKENS:
_add_token(env_tokens, token)
args.modelhub_token = modelhub_token args.modelhub_tokens = env_tokens
args.modelhub_token = env_tokens[0] if env_tokens else None
if args.hf_token: if args.hf_token:
os.environ["HF_TOKEN"] = args.hf_token os.environ["HF_TOKEN"] = args.hf_token
if args.modelscope_token:
os.environ["MODELSCOPE_API_TOKEN"] = args.modelscope_token
os.environ["MODELSCOPE_TOKEN"] = args.modelscope_token
for index, token in enumerate(args.modelhub_tokens, start=1):
env_name = "XC_TOKEN" if index == 1 else f"XC_TOKEN{index}"
os.environ[env_name] = token
if args.modelhub_token: if args.modelhub_token:
os.environ["MODELHUB_XC_TOKEN"] = args.modelhub_token os.environ["MODELHUB_XC_TOKEN"] = args.modelhub_token
os.environ["XC_TOKEN"] = args.modelhub_token os.environ["MODELHUB_XC_TOKENS"] = ",".join(args.modelhub_tokens)
if jwt_token: if not args.modelhub_token:
os.environ["MODELHUB_JWT_TOKEN"] = jwt_token raise ValueError("XC_TOKEN is required, either via environment or KEY.md")
if not args.modelhub_token and not jwt_token:
raise ValueError("MODELHUB_XC_TOKEN/XC_TOKEN or MODELHUB_JWT_TOKEN/JWT_TOKEN is required")

View File

@@ -9,7 +9,7 @@ from typing import Any, Callable
from common import parse_datetime, utc_now, write_json, write_jsonl from common import parse_datetime, utc_now, write_json, write_jsonl
from daily_runner import DEFAULT_DAILY_RUNS_DIR, log from daily_runner import DEFAULT_DAILY_RUNS_DIR, log
from history_stats import is_success, update_history_archive from history_stats import is_success, update_history_archive
from modelhub_client import ModelHubClient from modelhub_client import ModelHubClient, ModelHubClientPool
from models import HFModelSummary from models import HFModelSummary
from poll_runner import DEFAULT_POLL_RUNS_DIR, run_poll_loop from poll_runner import DEFAULT_POLL_RUNS_DIR, run_poll_loop
from runner_common import DEFAULT_KEY_PATH, ensure_tokens from runner_common import DEFAULT_KEY_PATH, ensure_tokens
@@ -63,7 +63,7 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true", action="store_true",
help="Accepted for backward compatibility; this runner always restricts to historically successful GPUs.", help="Accepted for backward compatibility; this runner always restricts to historically successful GPUs.",
) )
parser.add_argument("--min-downloads", type=int, default=50, help="Minimum Hugging Face download threshold") parser.add_argument("--min-downloads", type=int, default=50, help="Minimum ModelScope download threshold")
parser.add_argument( parser.add_argument(
"--history-stats-threshold", "--history-stats-threshold",
type=int, type=int,
@@ -72,7 +72,7 @@ def build_parser() -> argparse.ArgumentParser:
) )
parser.add_argument("--read-concurrency", type=int, default=4, help="Concurrency for read-only remote calls") parser.add_argument("--read-concurrency", type=int, default=4, help="Concurrency for read-only remote calls")
parser.add_argument("--dry-run", action="store_true", help="Plan the queue without creating tasks") parser.add_argument("--dry-run", action="store_true", help="Plan the queue without creating tasks")
parser.add_argument("--key-path", default=str(DEFAULT_KEY_PATH), help="Path to KEY.md containing HF_TOKEN/XC_TOKEN") parser.add_argument("--key-path", default=str(DEFAULT_KEY_PATH), help="Path to KEY.md containing MODELSCOPE_TOKEN/XC_TOKEN")
parser.add_argument("--runs-dir", default="runs", help=argparse.SUPPRESS) parser.add_argument("--runs-dir", default="runs", help=argparse.SUPPRESS)
parser.add_argument("--ledger-path", default="ledger/submissions.jsonl", help=argparse.SUPPRESS) parser.add_argument("--ledger-path", default="ledger/submissions.jsonl", help=argparse.SUPPRESS)
parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS) parser.add_argument("--history-archive-path", default="history/platform_tasks.jsonl", help=argparse.SUPPRESS)
@@ -80,10 +80,11 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--daily-runs-dir", default=str(DEFAULT_DAILY_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--daily-runs-dir", default=str(DEFAULT_DAILY_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--poll-runs-dir", default=str(DEFAULT_POLL_RUNS_DIR), help=argparse.SUPPRESS) parser.add_argument("--poll-runs-dir", default=str(DEFAULT_POLL_RUNS_DIR), help=argparse.SUPPRESS)
parser.add_argument("--outcomes-path", default="outcomes/submissions.jsonl", help=argparse.SUPPRESS) parser.add_argument("--outcomes-path", default="outcomes/submissions.jsonl", help=argparse.SUPPRESS)
parser.add_argument("--hf-base-url", default=os.getenv("HF_BASE_URL", "https://huggingface.co"), help=argparse.SUPPRESS) parser.add_argument("--hf-base-url", default=os.getenv("MODELSCOPE_BASE_URL", "https://modelscope.cn"), help=argparse.SUPPRESS)
parser.add_argument("--modelhub-base-url", default=os.getenv("MODELHUB_BASE_URL", "https://modelhub.org.cn"), help=argparse.SUPPRESS) parser.add_argument("--modelhub-base-url", default=os.getenv("MODELHUB_BASE_URL", "https://modelhub.org.cn"), help=argparse.SUPPRESS)
parser.add_argument("--modelhub-token", default=os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN"), help=argparse.SUPPRESS) parser.add_argument("--modelhub-token", default=os.getenv("MODELHUB_XC_TOKEN") or os.getenv("XC_TOKEN"), help=argparse.SUPPRESS)
parser.add_argument("--hf-token", default=os.getenv("HF_TOKEN"), help=argparse.SUPPRESS) parser.add_argument("--hf-token", default=os.getenv("HF_TOKEN"), help=argparse.SUPPRESS)
parser.add_argument("--modelscope-token", default=os.getenv("MODELSCOPE_API_TOKEN") or os.getenv("MODELSCOPE_TOKEN"), help=argparse.SUPPRESS)
parser.add_argument("--poll-interval-seconds", type=int, default=60, help="Sleep between polling cycles when no slots are available") parser.add_argument("--poll-interval-seconds", type=int, default=60, help="Sleep between polling cycles when no slots are available")
parser.add_argument("--idle-interval-seconds", type=int, default=30, help="Sleep between cycles when a scan submits nothing") parser.add_argument("--idle-interval-seconds", type=int, default=30, help="Sleep between cycles when a scan submits nothing")
parser.add_argument( parser.add_argument(
@@ -96,7 +97,10 @@ def build_parser() -> argparse.ArgumentParser:
return parser return parser
def make_modelhub_client(args: argparse.Namespace) -> ModelHubClient: def make_modelhub_client(args: argparse.Namespace) -> ModelHubClient | ModelHubClientPool:
tokens = list(getattr(args, "modelhub_tokens", None) or ([] if not args.modelhub_token else [args.modelhub_token]))
if len(tokens) > 1:
return ModelHubClientPool([ModelHubClient(token=token, base_url=args.modelhub_base_url) for token in tokens])
return ModelHubClient(token=args.modelhub_token, base_url=args.modelhub_base_url) return ModelHubClient(token=args.modelhub_token, base_url=args.modelhub_base_url)
@@ -115,6 +119,9 @@ def model_id_from_task(task: dict[str, Any]) -> str | None:
if value: if value:
return str(value).strip() return str(value).strip()
address = str(task.get("modelAddress") or "").strip().rstrip("/") address = str(task.get("modelAddress") or "").strip().rstrip("/")
marker = "modelscope.cn/models/"
if marker in address:
return address.split(marker, 1)[1].strip("/")
marker = "huggingface.co/" marker = "huggingface.co/"
if marker in address: if marker in address:
return address.split(marker, 1)[1].strip("/") return address.split(marker, 1)[1].strip("/")
@@ -243,7 +250,7 @@ def run_success_history_poll(
*, *,
now=None, now=None,
run_fn: Callable[..., dict[str, Any]] = run_poll_loop, run_fn: Callable[..., dict[str, Any]] = run_poll_loop,
modelhub_client: ModelHubClient | None = None, modelhub_client: ModelHubClient | ModelHubClientPool | None = None,
template_selector: TemplateSelector | None = None, template_selector: TemplateSelector | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
now = now or utc_now() now = now or utc_now()
@@ -315,8 +322,9 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv) args = parser.parse_args(argv)
ensure_tokens(args) ensure_tokens(args)
log( log(
f"[success-history] hf_token={'set' if bool(args.hf_token) else 'missing'} " f"[success-history] modelscope_token={'set' if bool(args.modelscope_token) else 'missing'} "
f"xc_token={'set' if bool(args.modelhub_token) else 'missing'} " f"xc_token={'set' if bool(args.modelhub_token) else 'missing'} "
f"xc_tokens={len(getattr(args, 'modelhub_tokens', []) or [])}"
) )
summary = run_success_history_poll(args) summary = run_success_history_poll(args)
print(f"poll_run_dir={summary['pollRunDir']}") print(f"poll_run_dir={summary['pollRunDir']}")