feat: ModelHub XC 适配智能体(/health + /stats + 批量提交 worker)

This commit is contained in:
user_4BTLNC
2026-10-25 02:37:55 +08:00
commit bda57181f8
7 changed files with 1020 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.git/
*.md

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.venv/
state.json

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM modelhubxc-4pd.tencentcloudcr.com/xc_agent_platform/python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PORT=8080
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
# 平台通过 /health 做 livenessProbe;SIGTERM 由 main.py 处理,30s 内退出
CMD ["python", "main.py"]

97
README.md Normal file
View File

@@ -0,0 +1,97 @@
# 信创模盒适配智能体(ModelHub XC Adapt Agent)
按《信创模盒适配智能体提交说明》(https://www.modelhub.org.cn/#/docs/6 )实现的策略镜像:
自动从社区模型库挑选候选模型,按卡批量提交国产算力适配验证任务。
## 一、平台契约对照
| 平台要求 | 本仓库实现 |
|---|---|
| 仓库根目录 Dockerfile | `Dockerfile`,基于 `modelhubxc-4pd.tencentcloudcr.com/xc_agent_platform/python:3.11-slim` |
| 暴露运行端口 | `EXPOSE 8080` |
| `GET /health` 返回 200 | `main.py` 内置 HTTP 服务,`/health` → `{"status":"ok"}` |
| 环境变量 `STRATEGY_ID` | 提交任务时填入请求体 `strategyId` |
| 优雅停机(SIGTERM,30s) | 收到信号置位 `STOP`,worker 退出后关闭服务,保存状态 |
| 资源 100m/256Mi ~ 1C/512Mi | 纯标准库、单线程 worker,常驻内存 < 40MB |
另外平台会注入 `EXTERNAL_SERVICE_TOKEN`(即个人主页的 xcToken),请求头用 `Xc-Token`。
## 二、目录结构
```
.
├── Dockerfile # 镜像构建
├── main.py # 服务入口:/health、/stats + 后台提交 worker
├── cards.py # 11 张目标算力卡的 GPU 标识、默认框架、configParams 模板
├── requirements.txt # 空文件(仅用 Python 标准库,无需第三方依赖)
└── README.md
```
`cards.py` 里的 `configParams` 直接取自平台网页端选择「适配成功率最高」框架后自动生成的 YAML,
与手工在页面提交时下发的配置完全一致,避免因启动命令/端口/显存参数写错导致验证失败。
## 三、业务流程
1. `POST /api/computility/models/list/page/vo` 拉模型库(22 个种子关键词 × 最多 5 页);
2. 过滤:仅 `文本生成`、已在 ≥`MIN_MACHINES` 张卡适配成功(说明仓库完整)、
排除 GGUF / AWQ / GPTQ / 量化(int4/int8/w8a8/w4a16/quantized/fp8/nvfp4)/ LoRA / adapter 等仓库;
3. 逐卡提交 `POST /api/adapt/task/add`,请求头 `Xc-Token`,请求体含 `strategyId`;
4. 返回码处理:
- `0` 成功,计入进度并落盘;
- `60027` 该卡已适配成功 → 跳过该组合;
- `60028` 该组合正在验证中 → 跳过该组合;
- `50000` 上游仓库拉取失败(模型已删除等)→ 跳过;
- `401/403/60014` 凭证或策略问题 → 记录错误并退避,不空转;
5. 每个模型最多提交 `QUOTA_PER_MODEL` 次(默认 2),避免同一模型刷满所有卡。
## 四、环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
| `STRATEGY_ID` | 平台注入 | 策略 ID,必填 |
| `XC_TOKEN` / `EXTERNAL_SERVICE_TOKEN` | 平台注入 | xcToken,请求头 `Xc-Token` |
| `TARGET_TASKS` | 250 | 本策略要提交成功的任务数 |
| `QUOTA_PER_MODEL` | 2 | 同一模型最多提交几次 |
| `MIN_MACHINES` | 2 | 模型至少已在几张卡适配成功才入选 |
| `INTERVAL_SEC` | 1.5 | 两次提交间隔 |
| `MAX_ATTEMPTS` | 4000 | 总尝试上限,防止无限循环 |
| `MODELHUB_API_BASE` | https://www.modelhub.org.cn | 接口域名 |
| `DRY_RUN` | 空 | 置 1 时不真正提交,用于本地自测 |
| `STATE_FILE` | /tmp/modelhub_agent_state.json | 进度落盘,重启可续跑 |
## 五、本地自测(不联网提交)
```bash
pip install -r requirements.txt
DRY_RUN=1 STRATEGY_ID=demo TARGET_TASKS=5 python main.py
curl http://127.0.0.1:8080/health # {"status":"ok"}
curl http://127.0.0.1:8080/stats # 进度、错误码分布、最近一次事件
```
## 六、上线步骤
1. **建仓库**:在 https://dev.modelhub.org.cn 新建仓库(例如 `xc-adapt-agent`),把本目录推上去:
```bash
git init && git add . && git commit -m "feat: modelhub xc adapt agent"
git remote add origin https://dev.modelhub.org.cn/<你的用户名>/xc-adapt-agent.git
git push -u origin main
```
2. **生成 xcToken**:官网个人主页 → 设置里生成/刷新 xcToken(该 token 由平台注入到容器)。
3. **注册智能体**:官网 → 个人主页 →「我的适配智能体」→「新增智能体」,
填写名称、仓库地址、标签,提交后列表出现该智能体。
4. **运行**:在列表点「运行」启动,点「停止」中止。运行后访问 `/stats` 查看提交进度:
```json
{"submitted": [...], "attempts": 63, "codes": {"0": 20, "60028": 41}, "progress": "20/250"}
```
## 七、注意事项
- 平台对「重复/低质提交」有监控,`TARGET_TASKS` 不建议一次性设得很大,可分日多轮运行;
- 提交成功的任务仍需平台排队验证,`/stats` 只统计**提交成功**,最终是否适配成功以任务列表为准;
- 若 `/stats` 出现 `60014`,说明 `Xc-Token` 或 `strategyId` 不匹配,检查 token 是否刷新、智能体是否已上线;
- 容器被 RESTART 后 worker 会读取 `STATE_FILE` 续跑,不会重复提交已成功的组合。

534
cards.py Normal file
View File

@@ -0,0 +1,534 @@
"""目标算力卡配置:卡名、平台内部 GPU 标识、默认框架与配置参数模板。
configParams 取自平台网页端选择「适配成功率最高」框架后自动生成的 YAML,
保证与手工提交时下发到验证机的配置完全一致。
"""
CARDS = [
{
"gpu": 'Biren_166m',
"label": '壁砺166M',
"framework": 'vllm_fix_tokenizer',
"configParams": (
"framework: vllm_fix_tokenizer\n"
"nv_framework: vllm_fix_tokenizer\n"
"lang: en\n"
"max_model_len: 4096\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
{
"gpu": 'Iluvatar_mrv-100',
"label": '智铠100',
"framework": 'vllm',
"configParams": (
"framework: vllm\n"
"nv_framework: vllm\n"
"max_model_len: 4096\n"
"lang: en\n"
"temperature: 0.4\n"
"repetition_penalty: 1.1\n"
"top_p: 0.9\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '20644'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --gpu-memory-utilization\n"
" - '0.9'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'"
),
},
{
"gpu": 'Iluvatar_bi-150',
"label": '天垓150',
"framework": 'vllm',
"configParams": (
"framework: vllm\n"
"nv_framework: vllm\n"
"api: completion\n"
"max_model_len: 4096\n"
"max_tokens: 1024\n"
"temperature: 0.7\n"
"repetition_penalty: 1.2\n"
"top_p: 0.9\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --gpu-memory-utilization\n"
" - '0.9'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'"
),
},
{
"gpu": 'Cambricon_mlu-370-x8',
"label": 'MLU370-X8',
"framework": 'vllm-mlu',
"configParams": (
"framework: vllm-mlu\n"
"nv_framework: vllm_fix_tokenizer\n"
"api: completion\n"
"lang: en\n"
"temperature: 0.4\n"
"repetition_penalty: 1.1\n"
"top_p: 0.9\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" env:\n"
" - name: MAX_MODEL_LEN\n"
" value: 4096\n"
" - name: VLLM_ALLOW_LONG_MAX_MODEL_LEN\n"
" value: '1'\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --trust-remote-code\n"
" - --dtype\n"
" - float16\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
{
"gpu": 'Sunrise_pt-200-x1',
"label": '曦望 S2',
"framework": 'vllm_fix_tokenizer',
"configParams": (
"framework: vllm_fix_tokenizer\n"
"nv_framework: vllm_fix_tokenizer\n"
"api: completion\n"
"max_tokens: 1024\n"
"temperature: 0.7\n"
"repetition_penalty: 1.2\n"
"top_p: 0.9\n"
"lang: zh\n"
"max_model_len: 4096\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --gpu-memory-utilization\n"
" - '0.9'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
{
"gpu": 'Kunlunxin_p-800',
"label": 'P800',
"framework": 'vllm_fix_tokenizer',
"configParams": (
"framework: vllm_fix_tokenizer\n"
"nv_framework: vllm_fix_tokenizer\n"
"lang: en\n"
"max_model_len: 4096\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
{
"gpu": 'MetaX_c-500',
"label": '曦云C500',
"framework": 'vllm_tokenizer_patch',
"configParams": (
"nv_framework: vllm_fix_tokenizer\n"
"framework: vllm_tokenizer_patch\n"
"storage: gpfs\n"
"max_tokens: 1024\n"
"max_model_len: 4096\n"
"api: completion\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command: ['/opt/conda/bin/vllm', 'serve', '/model', '--port', '8000', '--served-model-name', 'llm', '--max-model-len', '4096', '--gpu-memory-utilization', '0.9', '--enforce-eager', '--trust-remote-code' ,'-tp', '1' ]\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
{
"gpu": 'hygon_k100-ai',
"label": 'K100AI',
"framework": 'vllm',
"configParams": (
"framework: vllm\n"
"nv_framework: vllm\n"
"max_model_len: 4096\n"
"api: completion\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '20644'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'"
),
},
{
"gpu": 'Vastai_va16',
"label": 'VGX VA16',
"framework": 'vllm_fix_tokenizer',
"configParams": (
"framework: vllm_fix_tokenizer\n"
"nv_framework: vllm_fix_tokenizer\n"
"lang: en\n"
"max_model_len: 4096\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
{
"gpu": 'Iluvatar_bi-100',
"label": '天垓100',
"framework": 'vllm',
"configParams": (
"framework: vllm\n"
"nv_framework: vllm\n"
"lang: en\n"
"max_model_len: 4096\n"
"sut_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --gpu-memory-utilization\n"
" - '0.9'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '80'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --enforce-eager\n"
" - --trust-remote-code\n"
" - -tp\n"
" - '1'"
),
},
{
"gpu": 'Cambricon_mlu-370-x4',
"label": 'MLU370-X4',
"framework": 'vllm-mlu',
"configParams": (
"framework: vllm-mlu\n"
"nv_framework: vllm_fix_tokenizer\n"
"sut_config:\n"
" values:\n"
" gpu_num: 1\n"
" env:\n"
" - name: MAX_MODEL_LEN\n"
" value: 4096\n"
" command:\n"
" - vllm\n"
" - serve\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --trust-remote-code\n"
" - --dtype\n"
" - float16\n"
"ref_config:\n"
" gpu_num: 1\n"
" values:\n"
" command:\n"
" - /opt/entrypoint.sh\n"
" - /model\n"
" - --port\n"
" - '8000'\n"
" - --served-model-name\n"
" - llm\n"
" - --max-model-len\n"
" - '4096'\n"
" - --dtype\n"
" - float16\n"
" - --gpu-memory-utilization\n"
" - '0.95'\n"
" - -tp\n"
" - '1'\n"
" - --enforce-eager\n"
" - --trust-remote-code"
),
},
]
CARD_INDEX = {c["gpu"]: c for c in CARDS}

364
main.py Normal file
View File

@@ -0,0 +1,364 @@
"""信创模盒(ModelHub XC)适配智能体。
平台契约(见 https://modelhub.org.cn/#/docs/6):
- 仓库根目录必须有 Dockerfile,且 EXPOSE 一个端口用于健康检查
- 必须提供 GET /health,返回 200 表示存活
- 通过环境变量 STRATEGY_ID 获取自身策略 ID
- 必须正确处理 SIGTERM,30s 内完成退出
- 资源:requests 100m/256Mi,limits 1C/512Mi
业务逻辑:
1. 拉取社区模型库,筛出「文本生成 + 仓库完整 + 未在该卡适配过」的模型;
2. 按卡依次调用 /api/adapt/task/add 提交验证任务;
3. 命中 60027(该卡已验证成功)/ 60028(该卡验证中)就换下一个组合;
4. 直到提交满 TARGET_TASKS 条或模型池耗尽。
"""
from __future__ import annotations
import json
import os
import signal
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from cards import CARDS
# ---------------------------------------------------------------- 配置
API_BASE = os.getenv("MODELHUB_API_BASE", "https://www.modelhub.org.cn").rstrip("/")
STRATEGY_ID = os.getenv("STRATEGY_ID", "")
XC_TOKEN = os.getenv("XC_TOKEN") or os.getenv("EXTERNAL_SERVICE_TOKEN") or ""
TARGET_TASKS = int(os.getenv("TARGET_TASKS", "250"))
QUOTA_PER_MODEL = int(os.getenv("QUOTA_PER_MODEL", "2"))
INTERVAL_SEC = float(os.getenv("INTERVAL_SEC", "1.5"))
MAX_ATTEMPTS = int(os.getenv("MAX_ATTEMPTS", "4000"))
DRY_RUN = os.getenv("DRY_RUN", "").strip().lower() in ("1", "true", "yes", "on")
STATE_FILE = os.getenv("STATE_FILE", "/tmp/modelhub_agent_state.json")
POOL_FILE = os.getenv("POOL_FILE", "/tmp/modelhub_agent_pool.json")
PAGE_SIZE = int(os.getenv("PAGE_SIZE", "50"))
SEED_QUERIES = [q for q in os.getenv(
"SEED_QUERIES",
"Qwen2.5-0.5B,Qwen2.5-1.5B,Qwen2.5-3B,Qwen2.5-7B,Qwen3-0.6B,Qwen3-1.7B,Qwen3-4B,Qwen3-8B,"
"Llama-3.2,Llama-3.1,InternLM2.5,MiniCPM,Phi-3,Phi-4,gemma-2,Mistral-7B,Yi-1.5,GLM-4,"
"ChatGLM3,DeepSeek-R1-Distill,Qwen2-1.5B,Qwen2-7B",
).split(",") if q.strip()]
# 明显不能直接跑文本生成验证的仓库(统一转小写后匹配)
BAD_TOKENS = ("gensyn", "gguf", "awq", "gptq", "fp8", "fp4", "nvfp4", "adapter", "lora",
"int4", "int8", "w4a16", "w8a8", "w8a16", "quantized", "bnb", "exl2", "hqq",
"mlx", "onnx", "openvino", "tflite")
MIN_MACHINES = int(os.getenv("MIN_MACHINES", "2")) # 至少在一张卡上适配成功过,说明仓库完整
# ---------------------------------------------------------------- 全局状态
STOP = threading.Event()
LOCK = threading.Lock()
STATE = {
"started_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"submitted": [], # [{model, card, taskId, at}]
"attempts": 0,
"codes": {}, # code -> count
"last_event": None,
"finished": False,
"error": None,
}
def log(msg: str) -> None:
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
def _save_state() -> None:
try:
tmp = STATE_FILE + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(STATE, f, ensure_ascii=False, indent=1)
os.replace(tmp, STATE_FILE)
except Exception as exc: # 状态落盘失败不影响主流程
log(f"save state failed: {exc}")
def _load_state() -> None:
try:
with open(STATE_FILE, "r", encoding="utf-8") as f:
saved = json.load(f)
STATE["submitted"] = saved.get("submitted", [])
log(f"resume: {len(STATE['submitted'])} submitted")
except FileNotFoundError:
pass
except Exception as exc:
log(f"load state failed: {exc}")
# ---------------------------------------------------------------- HTTP
def _request(method: str, path: str, body: dict | None = None, timeout: int = 30) -> tuple[int, str]:
url = API_BASE + path
data = None
headers = {"Accept": "application/json", "User-Agent": "modelhub-xc-agent/1.0"}
if body is not None:
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
if XC_TOKEN:
headers["Xc-Token"] = XC_TOKEN
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode("utf-8", "replace")
except Exception as exc:
return 0, str(exc)
def _get_json(path: str, timeout: int = 30) -> dict:
status, text = _request("GET", path, timeout=timeout)
try:
return json.loads(text)
except Exception:
return {"code": -1, "message": f"HTTP {status}: {text[:200]}"}
def _post_json(path: str, body: dict, timeout: int = 60) -> dict:
status, text = _request("POST", path, body=body, timeout=timeout)
try:
return json.loads(text)
except Exception:
return {"code": -1, "message": f"HTTP {status}: {text[:200]}"}
# ---------------------------------------------------------------- 模型池
def build_pool() -> list[dict]:
"""拉模型库,返回 [{id, machines}],按「已适配卡数」升序(可选卡更多)。"""
seen: dict[str, dict] = {}
for query in SEED_QUERIES:
for page in range(1, 6):
payload = {"pageNum": page, "pageSize": PAGE_SIZE, "searchText": query}
res = _post_json("/api/computility/models/list/page/vo", payload, timeout=40)
records = (res.get("data") or {}).get("records") or []
if not records:
break
for item in records:
model_id = item.get("modelId") or ""
machines = [m.get("machineName") for m in (item.get("machines") or [])]
level = (item.get("taskLevelsInfo") or {}).get("taskLevelChineseName")
if not model_id or not machines or level != "文本生成":
continue
lower = model_id.lower()
if any(tok in lower for tok in BAD_TOKENS):
continue
if len(machines) < MIN_MACHINES:
continue
if model_id in seen:
continue
seen[model_id] = {"id": model_id, "machines": machines}
if len(records) < PAGE_SIZE:
break
if STOP.is_set():
break
pool = sorted(seen.values(), key=lambda x: len(x["machines"]))
log(f"pool built: {len(pool)} models")
try:
with open(POOL_FILE, "w", encoding="utf-8") as f:
json.dump(pool, f)
except Exception:
pass
return pool
def load_pool() -> list[dict]:
try:
with open(POOL_FILE, "r", encoding="utf-8") as f:
pool = json.load(f)
if pool:
log(f"pool loaded from cache: {len(pool)}")
return pool
except Exception:
pass
return build_pool()
# ---------------------------------------------------------------- 提交
def submit(model: str, card: dict) -> dict:
body = {
"modelAddress": f"https://huggingface.co/{model}",
"taskType": "text-generation",
"targetGpu": card["gpu"],
"framework": card["framework"],
"strategyId": STRATEGY_ID,
"configParams": card["configParams"],
}
if DRY_RUN:
return {"code": 0, "data": {"id": "dry-run"}, "dryRun": True}
return _post_json("/api/adapt/task/add", body)
def worker() -> None:
_load_state()
if not STRATEGY_ID:
STATE["error"] = "STRATEGY_ID 未注入,智能体无法提交(平台运行时自动注入)"
log(STATE["error"])
return
if not XC_TOKEN and not DRY_RUN:
STATE["error"] = "XC_TOKEN/EXTERNAL_SERVICE_TOKEN 未注入,请在个人主页生成 xcToken 并交给平台"
log(STATE["error"])
return
pool = load_pool()
submitted_pairs = {(s["model"], s["card"]) for s in STATE["submitted"]}
per_model: dict[str, int] = {}
for s in STATE["submitted"]:
per_model[s["model"]] = per_model.get(s["model"], 0) + 1
while not STOP.is_set() and len(STATE["submitted"]) < TARGET_TASKS:
progress = False
for card in CARDS:
if STOP.is_set() or len(STATE["submitted"]) >= TARGET_TASKS:
break
for cand in pool:
if STOP.is_set() or len(STATE["submitted"]) >= TARGET_TASKS:
break
if STATE["attempts"] >= MAX_ATTEMPTS:
log("reach MAX_ATTEMPTS, stop")
return
if card["gpu"] in cand["machines"]:
continue
if per_model.get(cand["id"], 0) >= QUOTA_PER_MODEL:
continue
pair = (cand["id"], card["gpu"])
if pair in submitted_pairs:
continue
res = submit(cand["id"], card)
code = res.get("code")
STATE["attempts"] += 1
STATE["codes"][str(code)] = STATE["codes"].get(str(code), 0) + 1
STATE["last_event"] = {
"at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"model": cand["id"], "gpu": card["gpu"], "code": code,
"message": (res.get("message") or "")[:160],
}
progress = True
if code == 0:
task_id = (res.get("data") or {}).get("id")
with LOCK:
STATE["submitted"].append({
"model": cand["id"], "card": card["gpu"],
"taskId": task_id,
"at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
})
per_model[cand["id"]] = per_model.get(cand["id"], 0) + 1
submitted_pairs.add(pair)
log(f"OK {len(STATE['submitted'])}/{TARGET_TASKS} | {card['label']} | {cand['id']} | {task_id}")
_save_state()
elif code == 60027: # 该卡已经验证成功过
submitted_pairs.add(pair)
elif code == 60028: # 该组合正在验证中
submitted_pairs.add(pair)
elif code == 50000: # 拉不到上游模型信息
submitted_pairs.add(pair)
elif code in (401, 403, 60014):
log(f"fatal {code}: {res.get('message')}")
STATE["error"] = f"{code}: {res.get('message')}"
_save_state()
STOP.wait(120)
elif code in (-1, 0) and res.get("message", "").startswith("HTTP"):
log(f"network issue: {res.get('message')}")
STOP.wait(10)
time.sleep(INTERVAL_SEC)
if not progress:
log("no progress this round; rebuild pool")
pool = build_pool()
if not pool:
log("empty pool, exit")
break
time.sleep(30)
STATE["finished"] = True
_save_state()
log(f"worker done: submitted={len(STATE['submitted'])} attempts={STATE['attempts']}")
# ---------------------------------------------------------------- HTTP 服务
class Handler(BaseHTTPRequestHandler):
server_version = "modelhub-xc-agent/1.0"
def do_GET(self) -> None: # noqa: N802
path = urllib.parse.urlparse(self.path).path
if path == "/health":
self._send({"status": "ok"})
return
if path == "/stats":
with LOCK:
payload = json.loads(json.dumps(STATE, ensure_ascii=False))
payload["target"] = TARGET_TASKS
payload["progress"] = f"{len(payload['submitted'])}/{TARGET_TASKS}"
payload["strategyId"] = STRATEGY_ID or None
payload["dryRun"] = DRY_RUN
self._send(payload)
return
if path == "/":
self._send({
"name": "modelhub-xc-adapt-agent",
"status": "stopped" if STOP.is_set() else "running",
"strategyId": STRATEGY_ID or None,
"target": TARGET_TASKS,
"submitted": len(STATE["submitted"]),
"attempts": STATE["attempts"],
"dryRun": DRY_RUN,
})
return
self._send({"error": "not found"}, status=404)
def log_message(self, fmt: str, *args) -> None:
print(f"{self.address_string()} - {fmt % args}", flush=True)
def _send(self, body: dict, status: int = 200) -> None:
payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def _handle_signal(signum: int, _frame) -> None:
log(f"received signal {signum}, shutting down")
STOP.set()
def main() -> None:
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
thread = threading.Thread(target=worker, name="submit-worker", daemon=True)
thread.start()
server = ThreadingHTTPServer(("0.0.0.0", int(os.getenv("PORT", "8080"))), Handler)
server.timeout = 1
log(f"agent listening on 0.0.0.0:{os.getenv('PORT', '8080')} (dryRun={DRY_RUN}, target={TARGET_TASKS})")
while not STOP.is_set():
server.handle_request()
thread.join(timeout=25)
server.server_close()
_save_state()
log("bye")
sys.exit(0)
if __name__ == "__main__":
main()

0
requirements.txt Normal file
View File