evalstone/DOCKER_BUILD.md
2026-07-21 09:32:49 +00:00

555 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# EvalScope Benchmark Docker 镜像构建实录
> 本文档记录 `evalscope-complete-py312` Docker 镜像的完整搭建过程、关键参数、踩坑与解决方案,供后续复现和维护参考。
---
## 1. 目标
构建一个**开箱即用**的 Docker 镜像,满足:
- 基于 Python 3.12
- 内置 evalscope 源码及全部依赖
-`swe_bench` 系列外,其他 benchmark 无需再安装任何东西即可运行
- 支持代码执行 sandboxhumaneval、bigcodebench
- 支持 Agent benchmarktau2_bench、general_fc
- 支持 function calling 评测bfcl_v3
- 在国内网络环境下可构建
---
## 2. 基础镜像选择
```dockerfile
FROM python:3.12-slim-bookworm
```
### 2.1 这句话是什么意思?
`FROM` 是 Dockerfile 的第一条指令,意思是:**我要基于哪个镜像开始构建**。
`python:3.12-slim-bookworm` 可以拆成三部分:
| 部分 | 含义 |
|------|------|
| `python` | 镜像名称,官方 Python 镜像 |
| `3.12` | Python 版本号 |
| `slim-bookworm` | 镜像变体标签 |
`slim` 表示精简版,只保留最基础的东西,体积小。
`bookworm` 是 Debian 12 的代号,这是 Linux 发行版的一个版本。
所以整句意思是:**基于官方 Python 3.12 精简版Debian 12镜像来构建我们的环境**。
类比理解:
> 就像你要装修房子,`FROM` 就是你选择的一套毛坯房。`python:3.12-slim-bookworm` 就是一套已经通了水电(装了 Python 和 pip、但还没放家具没装 evalscope 依赖)的毛坯房。
### 2.2 为什么选择这个镜像?
- **Python 3.12**:项目主力环境
- **slim**:体积小,比完整版少几百 MB
- **bookworm**Debian 12 稳定apt 包源丰富
- **已经包含 pip**:不需要自己装 Python
---
## 3. 关键构建参数
| 参数 | 值 | 说明 |
|------|-----|------|
| `DEBIAN_FRONTEND=noninteractive` | 环境变量 | 避免 apt 交互式提示 |
| `PYTHONUNBUFFERED=1` | 环境变量 | Python 输出不缓冲 |
| `PIP_NO_CACHE_DIR=1` | 环境变量 | 不保留 pip 缓存,减小镜像 |
| `PYTHONDONTWRITEBYTECODE=1` | 环境变量 | 不生成 .pyc |
| `PYTHONPATH=/opt/evalscope/evalscope` | 环境变量 | 解决 editable install 加载问题 |
| apt 源 | 清华镜像 | 国内加速 |
| pip 源 | 清华镜像 | 国内加速 |
| 基础包 | git/wget/curl/build-essential 等 | 编译依赖 |
| docker.io | apt 安装 | 容器内支持套 Docker 跑 sandbox |
---
## 4. Docker 命令参数详解(新手向)
如果你是 Docker 新手,先把下面几个概念和参数搞懂,后续命令就不难了。
### 4.1 核心概念
- **镜像Image**:一个只读的模板,相当于一个打包好的环境。比如 `evalscope-complete-py312:latest`
- **容器Container**:镜像运行起来的实例。你可以把镜像理解为 Class容器是 Object。
- **宿主机Host**:运行 Docker 的那台物理机/虚拟机。
- **容器内Container**Docker 容器里面的环境。
### 4.2 Dockerfile 常用指令
本项目 `Dockerfile.py312` 里用到的指令:
| 指令 | 作用 | 示例 |
|------|------|------|
| `FROM` | 基础镜像 | `FROM python:3.12-slim-bookworm` |
| `ENV` | 设置环境变量 | `ENV PYTHONPATH=/opt/evalscope/evalscope` |
| `RUN` | 执行命令(每行会产生一层镜像) | `RUN pip install ...` |
| `COPY` | 把宿主机文件复制到镜像里 | `COPY bash/ /opt/evalscope/bash/` |
| `WORKDIR` | 设置工作目录 | `WORKDIR /opt/evalscope` |
| `CMD` | 容器启动时默认执行的命令 | `CMD ["/bin/bash"]` |
### 4.3 `docker build` 参数
```bash
docker build -f Dockerfile.py312 -t evalscope-complete-py312:latest .
```
| 参数 | 含义 |
|------|------|
| `-f Dockerfile.py312` | 指定用哪个 Dockerfile默认是当前目录的 `Dockerfile` |
| `-t evalscope-complete-py312:latest` | 给构建好的镜像打标签,`name:tag` 格式 |
| `.` | 构建上下文路径Docker 会把这个目录下的文件传给构建进程 |
### 4.4 `docker run` 参数
这是用得最多的命令:
```bash
docker run -it --rm \
--network host \
-v /data1/sora/evalscope/datasets:/opt/evalscope/datasets \
-v /data1/sora/evalscope/output:/opt/evalscope/output \
-v /var/run/docker.sock:/var/run/docker.sock \
evalscope-complete-py312:latest \
bash -c "cd /opt/evalscope && python bash/run_lite.py ..."
```
| 参数 | 含义 |
|------|------|
| `docker run` | 创建并启动一个容器 |
| `-i` | 交互模式,保持 STDIN 打开 |
| `-t` | 分配一个伪终端,让你能看到彩色输出 |
| `-it` | 上面两个一起用,几乎必加 |
| `--rm` | 容器停止后自动删除,避免垃圾容器堆积 |
| `--network host` | 容器和宿主机共用网络,`localhost` 指向宿主机 |
| `-v 宿主机路径:容器内路径` | 挂载目录/文件,容器内的改动会反映到宿主机 |
| `-e KEY=VALUE` | 设置容器内环境变量 |
| `evalscope-complete-py312:latest` | 要运行的镜像名 |
| `bash -c "..."` | 容器启动后执行的命令 |
**挂载参数 `-v` 是本项目的核心**
```bash
-v /data1/sora/evalscope/datasets:/opt/evalscope/datasets
```
意思是:把宿主机的 `/data1/sora/evalscope/datasets` 映射到容器内的 `/opt/evalscope/datasets`。这样容器就能读取宿主机上的数据,评测结果也能写回宿主机。
### 4.5 其他常用命令
```bash
# 查看本地镜像
docker images
# 查看运行中的容器
docker ps
# 查看所有容器(包括停止的)
docker ps -a
# 删除镜像
docker rmi evalscope-complete-py312:latest
# 删除容器
docker rm 容器ID
# 进入正在运行的容器
docker exec -it 容器ID /bin/bash
# 加载 tar.gz 镜像
docker load -i evalscope-complete-py312.tar.gz
# 导出镜像为 tar.gz
docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
```
### 4.6 为什么这个项目不需要 `--gpus all`
很多 Docker + GPU 的教程会写 `--gpus all`,但**本项目不需要**。
原因是:
- evalscope 容器本身只做 API 调用,不跑模型推理。
- 模型服务sglang是在宿主机上启动的。
- 容器只需要通过网络访问 `http://localhost:30000/v1`
所以用 `--network host` 就够了,不需要把 GPU 分配给容器。
---
## 5. 构建步骤
### 5.1 准备构建上下文
#### `tools/docker/` 这个文件夹里都要放什么?
```
tools/docker/
├── Dockerfile.py312 # 构建配方
├── evalscope/ # evalscope 源码(从项目根目录同步过来)
├── bash/ # bash 脚本(从项目根目录同步过来)
└── tau2-bench/ # tau2-bench 源码(从项目根目录同步过来)
```
**为什么需要同步?**
因为 Docker 构建时只能访问 `tools/docker/` 这个目录下的文件(这叫**构建上下文**)。而项目的源码在 `/data1/sora/evalscope/` 根目录下,所以需要先把需要的部分复制到 `tools/docker/` 里。
#### `rsync` 命令是什么意思?
```bash
rsync -av --delete --exclude='*.log' --exclude='__pycache__' \
bash/ tools/docker/bash/
```
拆开看:
| 参数 | 含义 |
|------|------|
| `rsync` | 文件同步工具 |
| `-a` | 归档模式,保留文件权限、时间等 |
| `-v` | 显示同步了哪些文件 |
| `--delete` | 删除目标目录中源目录没有的文件,保持完全一致 |
| `--exclude='*.log'` | 不同步 `.log` 日志文件 |
| `--exclude='__pycache__'` | 不同步 Python 缓存目录 |
| `bash/` | 源目录(项目根目录的 bash 脚本) |
| `tools/docker/bash/` | 目标目录(构建上下文中的位置) |
**用人话讲**:把 `bash/` 里的文件同步到 `tools/docker/bash/`,去掉日志和缓存,并且保证两边完全一样。
#### 为什么用 rsync 而不用 `cp -r`
因为 `cp -r` 只是简单复制,不会删除目标目录里多余的旧文件。如果用 `cp -r`,以前删除的脚本可能还会留在 `tools/docker/bash/` 里,最终被打包进镜像。
### 5.2 构建镜像
```bash
cd /data1/sora/evalscope/tools/docker
docker build -f Dockerfile.py312 -t evalscope-complete-py312:latest .
```
### 5.3 环境是怎么装进 Docker 里的?
很多新手会问Python 包是怎么装到镜像里的?答案是:**Dockerfile 里的 `RUN pip install ...` 命令会在构建时执行,把包装到镜像里**。
整个流程可以概括为:
```
毛坯房python:3.12-slim-bookworm
装修第一层换国内源、装系统工具apt
装修第二层RUN pip install ... 装 Python 包
装修第三层COPY 项目源码进镜像
装修第四层:把源码用 pip install -e 安装好
装修第五层:设置 PYTHONPATH、权限等收尾工作
完工evalscope-complete-py312:latest 镜像)
```
关键步骤对应 Dockerfile 里的这些行:
| Dockerfile 行 | 作用 |
|---------------|------|
| `FROM python:3.12-slim-bookworm` | 选毛坯房 |
| `RUN apt-get update && apt-get install ...` | 装系统工具 |
| `RUN pip install openai pandas ...` | 装 Python 包 |
| `COPY evalscope/ /opt/evalscope/evalscope/` | 复制源码 |
| `RUN pip install -e /opt/evalscope/evalscope/` | 安装 evalscope 本身 |
| `ENV PYTHONPATH=/opt/evalscope/evalscope` | 设置环境变量 |
**`RUN``COPY` 的区别**
- `RUN`:执行命令,比如安装软件
- `COPY`:复制文件,比如把源码复制进去
**为什么先 `pip install` 再 `COPY` 源码?**
因为 Docker 有缓存机制。如果依赖没有变化只有源码变化Docker 会直接使用之前装好的依赖层,只重新构建 COPY 和之后的层,节省大量时间。
### 5.4 导出 tar.gz
```bash
docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
md5sum evalscope-complete-py312.tar.gz > evalscope-complete-py312.tar.gz.md5
```
---
## 6. 安装的关键依赖
### 6.1 evalscope 核心依赖
```bash
pip install \
openai pandas numpy pyyaml requests tqdm \
tiktoken transformers \
scikit-learn matplotlib seaborn plotly \
jieba nltk rouge-score sacrebleu \
sympy latex2sympy2_extended pillow \
docker pexpect pytest \
tabulate rich jsonlines jsonschema \
langdetect word2number zhconv \
modelscope pydantic overrides \
more_itertools pylatexenc \
rouge-chinese markdown \
editdistance dotenv docstring_parser \
colorlog
```
### 6.2 Sandbox 支持
```bash
pip install evalscope[sandbox] 2>/dev/null || pip install ms-sandbox 2>/dev/null || true
```
`ms-sandbox` 是 ModelScope 的 sandbox 实现,用于 humaneval、bigcodebench 的隔离代码执行。
### 6.3 terminal_bench 依赖
```bash
pip install "harbor>=0.8.0,<1.0.0"
```
### 6.4 tau2-bench 依赖
```bash
pip install \
fastapi uvicorn psutil loguru \
litellm tenacity deepdiff addict toml
# 同时把本地 tau2-bench 源码 editable 安装
pip install -e /opt/evalscope/tools/tau2-bench/ --no-deps
```
### 6.5 bigcodebench 评估依赖
```bash
pip install \
tree-sitter tree-sitter-python tempdir termcolor wget gradio-client
```
### 6.6 bfcl_v3 依赖
```bash
pip install --no-deps bfcl-eval==2025.10.27.1
pip install \
anthropic cohere==5.18.0 datamodel-code-generator==0.25.7 \
faiss-cpu==1.11.0 google-genai==1.24.0 mistralai==1.7.0 \
networkx==3.3 numpy==1.26.4 google-search-results \
rank_bm25 html2text boto3 qwen-agent writer-sdk \
tree-sitter tree-sitter-python tree-sitter-javascript tree-sitter-java
```
注意:`bfcl-eval` 默认会装 torch这里用 `--no-deps` 避免把 torch 拉进来,再手动补缺少的依赖。
### 6.7 其他工具
```bash
pip install soundfile openpyxl
```
---
## 7. 踩坑记录
### 7.1 editable install 加载失败
**现象**
```
ImportError: cannot import name 'run_task' from 'evalscope' (unknown location)
```
**原因**evalscope 用 `pip install -e /opt/evalscope/evalscope/` 安装后editable finder 有时不能正确加载 `__init__.py`
**解决**:在 Dockerfile 末尾设置 `ENV PYTHONPATH=/opt/evalscope/evalscope`
### 7.2 镜像里缺少新脚本
**现象**:其他机器拉下来跑 `run_group2.py``No such file or directory`
**原因**Docker 镜像构建后,项目根目录又新增了 `run_group1/2/3.py``run_1.py` 等文件,但镜像没有重新构建。
**解决**:每次修改 bash/ 目录后,必须重新同步到 `tools/docker/bash/`,然后重新 `docker build``docker save`
### 7.3 镜像体积过大
**现象**:安装 bfcl-eval 默认会拉 torch镜像暴涨几个 GB。
**解决**
```bash
pip install --no-deps bfcl-eval==2025.10.27.1
```
再手动安装缺少的非 torch 依赖。
### 7.4 litellm 版本冲突
**现象**tau2-bench 要求 `litellm>=1.80.15,<1.82.7`,但 bfcl-eval 可能装更高版本。
**解决**:在 Dockerfile 中显式约束:
```bash
pip install "litellm>=1.80.15,<1.82.7" "tenacity>=9.0.0"
```
### 7.5 国内网络下载慢/超时
**现象**pip、apt、git clone 都很慢。
**解决**
- apt 换清华源
- pip 换清华源
- git+https 安装 tau2-bench 时容易失败,改为本地 COPY 源码后 editable 安装
### 7.6 ModelScope 上传 API 参数错误
**现象**
```
TypeError: HubApi.upload_file() got an unexpected keyword argument 'model_id'
```
**原因**:新版 ModelScope SDK 参数名是 `repo_id`,不是 `model_id`
**解决**
```python
api.upload_file(
repo_id='SoraAmami/evalscope-docker',
path_or_fileobj='evalscope-complete-py312.tar.gz',
path_in_repo='evalscope-complete-py312.tar.gz'
)
```
### 7.7 数据集路径重复
**现象**`/data1/sora/evalscope/datasets/datasets/` 下又有一层数据。
**原因**evalscope 会在 `dataset_dir` 后自动拼 `datasets/`。如果把 `dataset_dir` 指向了已经包含 `datasets/` 的路径,就会重复。
**解决**:始终让 `dataset_dir` 指向 `datasets/` 的父目录。
### 7.8 运行时缺少 tokenizer
**现象**`transformers` 提示 `PyTorch was not found`
**原因**:镜像里没有 torch intentional为了减小体积
**影响**evalscope 的 tokenizer 加载通常只需要 `transformers` 的 tokenizer 部分,实际运行时通过 API 调用模型,不需要本地 PyTorch。该警告可忽略。
---
## 8. 镜像验证
构建完成后,验证镜像内关键组件:
```bash
docker run --rm evalscope-complete-py312:latest bash -c "
python -c 'import evalscope; print(evalscope.__file__)' && \
python -c 'import evalscope.api.agent' && \
python -c 'import docker' && \
python -c 'import harbor' && \
python -c 'import tau2' && \
python -c 'import bfcl_eval' && \
python -c 'import soundfile' && \
ls -la /opt/evalscope/bash/run_1.py /opt/evalscope/bash/run_group*.py
"
```
---
## 9. 三个评测版本
为了兼顾快速验证和完整评测,提供三个版本:
| 版本 | 预计时间 | 用途 |
|------|----------|------|
| **Lite** | ~2-4h | 快速冒烟,每个能力域 1-2 个 benchmark |
| **Standard** | ~20-28h | 常规能力评测,覆盖主要 benchmark |
| **Full** | ~3-5 天 | 完整评测,全部 benchmark + 多次采样 |
每个版本都覆盖 5 大能力域:代码生成、推理/数学、知识、长上下文、智能体/工具。
### 9.1 Lite 版
- 代码:`humaneval`
- 推理/数学:`aime24``gsm8k`
- 知识:`mmlu_pro``simple_qa`
- 长上下文:`longbench_v2`
- 智能体/工具:`bfcl_v3`
运行:
```bash
python bash/run_lite.py --model YourModel --api-url http://localhost:30000/v1 --limit 20
```
### 9.2 Standard 版
- 代码:`humaneval``live_code_bench``bigcodebench`
- 推理/数学:`aime24``aime25``aime26``hmmt26``gsm8k``competition_math``bbh``drop`
- 知识:`gpqa_diamond``mmlu_pro``simple_qa``super_gpqa``mmlu``cmmlu``arc``hellaswag``trivia_qa``winogrande`
- 长上下文:`longbench_v2``openai_mrcr`
- 智能体/工具:`tau2_bench``general_fc``bfcl_v3`
运行:
```bash
python bash/run_standard.py --model YourModel --api-url http://localhost:30000/v1 --limit 100
```
### 9.3 Full 版
覆盖 `run.py` 中全部 benchmark使用完整数据集和多次采样配置。
运行:
```bash
python bash/run.py --model YourModel --api-url http://localhost:30000/v1 --limit none
```
---
## 10. 文件清单
```
tools/docker/
├── Dockerfile.py312 # 镜像构建定义
├── evalscope-complete-py312.tar.gz # 导出的镜像
├── evalscope-complete-py312.tar.gz.md5
├── deploy.sh # 目标机器部署脚本
├── export_image.sh # 导出脚本
├── build_*.log # 构建日志
└── README.md # 使用说明
```
---
## 11. 重新构建流程
```bash
cd /data1/sora/evalscope
# 1. 同步最新代码
rsync -av --delete bash/ tools/docker/bash/
rsync -av --delete evalscope/ tools/docker/evalscope/
rsync -av --delete tau2-bench/ tools/docker/tau2-bench/
# 2. 构建
cd tools/docker
docker build -f Dockerfile.py312 -t evalscope-complete-py312:latest .
# 3. 导出
docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
md5sum evalscope-complete-py312.tar.gz > evalscope-complete-py312.tar.gz.md5
# 4. 上传到 ModelScope可选
python3 -c "
from modelscope.hub.api import HubApi
api = HubApi()
api.login('ms-3d554a39-6e07-496d-8022-0b0ee64a6389')
api.upload_file(
repo_id='SoraAmami/evalscope-docker',
path_or_fileobj='evalscope-complete-py312.tar.gz',
path_in_repo='evalscope-complete-py312.tar.gz'
)
"
```