feat: ModelHub XC 适配智能体(/health + /stats + 批量提交 worker)
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.git/
|
||||
*.md
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
state.json
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal 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
97
README.md
Normal 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
534
cards.py
Normal 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
364
main.py
Normal 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
0
requirements.txt
Normal file
Reference in New Issue
Block a user