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