- EvalScope 离线评测部署简记(华为910B ARM + OpenAI 兼容接口)
- 背景
- 思路
- 为什么镜像和数据分开?
- 为什么不用 EvalScope 自带的 dataset-hub?
- 为什么分两个脚本?
- 一、Mac 端准备(构建镜像与下载数据集)
- 1.1 前置条件
- 1.2 Dockerfile 结构
- 1.3 依赖说明
- 1.4 一键构建脚本
- 1.5 目录结构与产物
- 二、产物说明与传输
- 2.1 文件传输
- 三、覆盖维度与数据集
- 四、910B 离线部署
- 4.1 加载镜像
- 4.2 目录规划
- 4.3 前提:模型推理服务
- 4.4 运行评测
- 一、知识类(Knowledge)
- 二、推理类(Math/reasoning)
- 三、代码类(Coding)
- 四、Agent/工具调用
- 4.5 直接 CLI 方式(不用脚本)
- 5.2 POST /v1/chat/completions
- 六、非标准接口适配方案
- 七、冒烟验证
- 八、踩坑记录
- 8.1 dataset_dir 路径错误
- 8.2 不要用 :ro 只读挂载
- 8.3 evalscope 命令行规范
- 8.4 推荐用法:dataset_id 指定本地路径
- 8.5 BFCL-v4 数据来源确认
- 8.6 数据集必须全部传输
- 九、注意事项
- 十、构建链路踩坑补记(EvalScope 1.8/1.9 + 国内网络)
- 10.1 pip 直连 pypi.org 被 TLS 掐断 → 换清华源
- 10.2 datasets 版本冲突(ResolutionImpossible,别被 fsspec 带偏)
- 10.3 tau-bench 的 GitHub 安装在国内不可靠 → vendored 进上下文
- 10.4 arm64 交叉构建实测(Intel Mac + QEMU)
- 10.5 Docker Desktop 环境层面的两个坑
- 10.6 冒烟测试中发现的额外坑
- 10.7 datasets 4.0.0 Value dtype 兼容补丁(2026-07-13)
- 10.8 Dockerfile 行内 Python 多行语法陷阱
EvalScope 离线评测部署简记(华为910B ARM + OpenAI 兼容接口)
背景
在华为 910B (ARM64) 断网服务器上跑 EvalScope 评测框架。Intel Mac 上交叉准备 ARM64 镜像和数据集,打包后 scp 导入即可。覆盖通用知识、数学推理、代码、Agent/工具调用四个维度,八个标准数据集。
评测框架:EvalScope v1.7.1,ModelScope 社区维护。
重要更新(2026-06-10):实际部署中发现若干配置陷阱,已修正并补充脚本化方案,见第八节踩坑记录。经实际验证,BFCL-v4 数据确实内置在 bfcl-eval 包中(12MB),无需额外下载。
思路
整个离线方案要解决三个核心矛盾:
- 断网 vs 数据集下载:数据集在 ModelScope/HuggingFace 上,NPU 节点无外网。思路是在 Mac 上预先下载,打包后 scp 传输,通过 Volume 挂载进容器。
- ARM64 vs x86 Mac:910B 是 ARM64,Mac 是 Intel x86。用 Docker Buildx 交叉编译 ARM64 镜像,这样容器内的 Python 环境(Python 3.10 + ARM64 依赖)和 NPU 原生环境一致。
- 多种数据集 vs 统一入口:八个数据集分属四个维度,每个参数不同(few-shot 数量、超时、子集选择)。如果全写死一个命令,要么太冗余要么跑不全。方案是用两个脚本 + 环境变量驱动,按维度分类调用。
为什么镜像和数据分开?
镜像约 10GB,数据集约 700MB。如果合在一起,每次更新数据集都需要重新构建整个镜像——耗时且浪费传输带宽。分开后,镜像只需构建一次,数据集独立更新、挂载即可。
为什么不用 EvalScope 自带的 dataset-hub?
EvalScope 默认通过 ModelScope Hub 自动下载数据集到 --dataset-dir 指定目录。这在断网环境有两个问题:
- 找不到缓存时会尝试联网下载 → DNS 超时报错
- 缓存目录结构(
{org}/{repo}/)和 EvalScope 的默认查找路径(~/.cache/modelscope/hub/datasets/)不一致,容易出现路径匹配失败
所以直接用 dataset_id 参数指定本地绝对路径,完全绕过 Hub 机制,离线无依赖。
为什么分两个脚本?
run_eval.sh 处理标准数据集(mmlu、ceval、humaneval 等),这些数据集需要挂载 modelscope_cache 目录,有 few_shot_num、subset_list 等参数。
run_agent_eval.sh 处理 Agent 数据集(bfcl_v4、tau_bench),这些数据集数据内置在 Python 包中,不需要额外挂载,但需要特殊的 extra_params(如 tau_bench 的用户模拟模型配置)。
两类数据集的挂载需求、参数结构完全不同,分开比混在一起更清晰。
一、Mac 端准备(构建镜像与下载数据集)
Intel Mac 上通过 Docker Buildx 交叉编译 ARM64 镜像。核心依赖是 QEMU 多架构模拟层。
1.1 前置条件
# Docker Desktop 4.47+(支持 macOS 13.7 + buildx + QEMU)
docker --version # Docker version 28.x+
docker buildx version # 必须支持 --platform linux/arm64
# 注册 QEMU 多架构支持(只需要执行一次,重启后需重新执行)
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# 验证 ARM64 构建可用
docker buildx inspect --bootstrap
# 应看到 Platforms: linux/arm64, linux/amd64 等
1.2 Dockerfile 结构
FROM python:3.10-slim-bookworm # ARM64 Python 基础镜像
# 环境变量预设离线和缓存路径
ENV PYTHONUNBUFFERED=1 \
EVALSCOPE_CACHE=/workspace/cache/evalscope \
MODELSCOPE_CACHE=/workspace/cache/modelscope/hub \
HF_HOME=/workspace/cache/huggingface \
HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1
# 安装构建依赖 + docker.io(SWE-bench 需要)+ git/git-lfs
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl docker.io \
git git-lfs jq openssh-client patch tini \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(evalscope + bfcl-eval + swebench + tau_bench)
COPY requirements.txt /workspace/requirements.txt
RUN pip install --upgrade pip && pip install -r /workspace/requirements.txt
# 拷入评测脚本和配置
COPY scripts /workspace/scripts
COPY configs /workspace/configs
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["bash"]
# Docker Desktop 4.47+(支持 macOS 13.7 + buildx + QEMU)
docker --version # Docker version 28.x+
docker buildx version # 必须支持 --platform linux/arm64
# 注册 QEMU 多架构支持(只需要执行一次,重启后需重新执行)
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# 验证 ARM64 构建可用
docker buildx inspect --bootstrap
# 应看到 Platforms: linux/arm64, linux/amd64 等
FROM python:3.10-slim-bookworm # ARM64 Python 基础镜像
# 环境变量预设离线和缓存路径
ENV PYTHONUNBUFFERED=1 \
EVALSCOPE_CACHE=/workspace/cache/evalscope \
MODELSCOPE_CACHE=/workspace/cache/modelscope/hub \
HF_HOME=/workspace/cache/huggingface \
HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1
# 安装构建依赖 + docker.io(SWE-bench 需要)+ git/git-lfs
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl docker.io \
git git-lfs jq openssh-client patch tini \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(evalscope + bfcl-eval + swebench + tau_bench)
COPY requirements.txt /workspace/requirements.txt
RUN pip install --upgrade pip && pip install -r /workspace/requirements.txt
# 拷入评测脚本和配置
COPY scripts /workspace/scripts
COPY configs /workspace/configs
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["bash"]
几个设计考量:
- 选 Python 3.10-slim 而非 3.12/3.13:EvalScope 1.7.1 的依赖(特别是 bfcl-eval 对 faiss-cpu==1.11.0 的硬编码要求)在 Python 3.12+ 上存在兼容问题,3.10 是最稳妥的选择。
- 安装 docker.io:SWE-bench 评测需要在容器内调用 Docker 来构建和运行测试环境,因此镜像内需要 Docker CLI(通过 -v /var/run/docker.sock 与宿主机 Docker daemon 通信)。
- HF_HUB_OFFLINE=1:强制 HuggingFace 库进入离线模式,即使代码中有联网尝试也会直接使用本地缓存,避免意外下载。
- tini 作为 ENTRYPOINT:轻量级 init 进程,处理信号转发和僵尸进程回收,对长时间运行的评测任务比较友好。
1.3 依赖说明
evalscope[swe-bench,bfcl]==1.7.1 # EvalScope 核心 + SWE-bench + BFCL 插件
modelscope>=1.31.0 # ModelScope SDK(下载数据集用)
datasets>=3.0.0 # HuggingFace datasets 库
bfcl-eval==2025.10.27.1 # BFCL-v4 评测引擎(数据内置)
swebench==4.1.0 # SWE-bench 评测引擎
git+https://github.com/sierra-research/tau-bench # tau_bench 用户模拟
evalscope[swe-bench,bfcl]==1.7.1 # EvalScope 核心 + SWE-bench + BFCL 插件
modelscope>=1.31.0 # ModelScope SDK(下载数据集用)
datasets>=3.0.0 # HuggingFace datasets 库
bfcl-eval==2025.10.27.1 # BFCL-v4 评测引擎(数据内置)
swebench==4.1.0 # SWE-bench 评测引擎
git+https://github.com/sierra-research/tau-bench # tau_bench 用户模拟
evalscope[swe-bench,bfcl] 是 EvalScope 的 extras 安装方式——swe-bench 附加 SWE-bench 适配器,bfcl 附加 BFCL 适配器。
1.4 一键构建脚本
cd evalscope_offline
bash scripts/download_assets.sh
cd evalscope_offline
bash scripts/download_assets.sh
这个脚本按顺序执行:
- 下载 Python wheels 到
wheels/(可选,Mac ARM64 交叉下载可能失败,此时跳过不影响后续) - 下载 ModelScope 数据集到
modelscope_cache/:调用 ModelScopesnapshot_download下载全部 7 个数据集,约 5-10 分钟(取决于网络) - 构建 ARM64 Docker 镜像:
docker buildx build --platform linux/arm64 -t evalscope-offline:arm64 . --load,约 15-30 分钟(取决于网络和 CPU) - 导出镜像为 tar.gz:
docker save | gzip,产物约 6GB
如果步骤 1 失败,可以跳过:
# 手动分步执行
python3 scripts/download_datasets.py --cache-dir modelscope_cache
docker buildx build --platform linux/arm64 \
-t evalscope-offline:arm64 \
-f Dockerfile.arm64 . --load
docker save evalscope-offline:arm64 | gzip -c > images/evalscope-offline-arm64.tar.gz
1.5 目录结构与产物
构建完成后 Mac 上的目录:
evalscope_offline/
├── Dockerfile.arm64 # ARM64 镜像定义
├── requirements.txt # Python 依赖
├── modelscope_cache/ # 数据集 (709MB) — 需要 scp 到 NPU
│ ├── cais/mmlu/
│ ├── evalscope/ceval/
│ ├── AI-ModelScope/gpqa_diamond/
│ ├── ZhipuAI/LongBench-v2/
│ ├── knoveleng/Minerva-Math/
│ ├── opencompass/humaneval/
│ └── evalscope/swe-bench-verified-mini/
├── images/
│ └── evalscope-offline-arm64.tar.gz # Docker 镜像 (6GB) — 需要 scp 到 NPU
├── scripts/ # 评测脚本 — scp 到 NPU
│ ├── run_eval.sh
│ ├── run_agent_eval.sh
│ └── run_smoke_mock_api.py
├── configs/ # 参考配置
│ ├── 910b_eval_core.yaml
│ ├── 910b_eval_agent.yaml
│ └── 910b_eval_swe.yaml
└── wheels/ # Python 离线安装包(可选)
二、产物说明与传输
三个文件需要传到 NPU:
1. images/evalscope-offline-arm64.tar.gz (6.2GB) Docker 镜像
2. modelscope_cache/ (709MB) 数据集缓存
3. scripts/ 评测脚本
其中,BFCL-v4 和 tau_bench 的评测数据分别内置在 bfcl-eval pip 包和 tau-bench git 包中,Docker 镜像已包含,无需额外传输。
2.1 文件传输
# 从 Mac 传到 910B
scp images/evalscope-offline-arm64.tar.gz user@910b:/data/eval/
scp -r modelscope_cache/ user@910b:/data/eval/
scp -r scripts/ user@910b:/data/eval/
三、覆盖维度与数据集
# 从 Mac 传到 910B
scp images/evalscope-offline-arm64.tar.gz user@910b:/data/eval/
scp -r modelscope_cache/ user@910b:/data/eval/
scp -r scripts/ user@910b:/data/eval/
| 维度 | 数据集 | 样本量 | 评测指标 | few-shot |
|---|---|---|---|---|
| 通用知识 | MMLU | ~14k | Accuracy | 5 |
| 通用知识 | C-Eval | ~14k | Accuracy | 5 |
| 通用知识 | GPQA Diamond | 198 | Accuracy | 0 |
| 数学推理 | Minerva-Math | 272 | Accuracy | 0 |
| 长文本推理 | LongBench-v2 | 100 | Accuracy | - |
| 代码 | HumanEval | 164 | pass@1 | 0 |
| 代码 | SWE-bench-mini | 50 | resolve rate | 0 |
| Agent/工具调用 | BFCL-v4 | ~1700 | Accuracy | 0 |
| Agent | tau_bench | 多域 | Task Success | 0 |
四、910B 离线部署
4.1 加载镜像
ssh user@910b
cd /data/eval
docker load -i evalscope-offline-arm64.tar.gz
# 验证
docker images evalscope-offline:arm64
# 输出: evalscope-offline:arm64 ... ~10GB
4.2 目录规划
mkdir -p /data/eval/reports /data/eval/outputs
# 最终结构
/data/eval/
├── modelscope_cache/ # 核心数据集(不加 :ro)
├── reports/ # 评测报告输出
├── outputs/ # 中间结果
└── scripts/ # 评测脚本
ssh user@910b
cd /data/eval
docker load -i evalscope-offline-arm64.tar.gz
# 验证
docker images evalscope-offline:arm64
# 输出: evalscope-offline:arm64 ... ~10GB
mkdir -p /data/eval/reports /data/eval/outputs
# 最终结构
/data/eval/
├── modelscope_cache/ # 核心数据集(不加 :ro)
├── reports/ # 评测报告输出
├── outputs/ # 中间结果
└── scripts/ # 评测脚本
为什么要 --network host? 容器需要访问宿主机的模型推理服务(910B NPU 上运行的 API)。如果用默认桥接网络,容器内访问宿主 IP 需要额外配置。--network host 最简单可靠。
为什么 modelscope_cache 不加 :ro? EvalScope 运行时会往数据集目录写入缓存索引(Arrow/Parquet 格式首次加载时框架会生成索引文件)。只读挂载会导致 Permission denied,评测直接报错。虽然数据集本身不需要被修改,但框架需要写权限建立索引。如果确实不想让数据集目录被修改,可以 cp -r 一份副本再挂载。
4.3 前提:模型推理服务
910B 上必须运行一个 OpenAI 兼容的 /v1 API:
curl http://127.0.0.1:8000/v1/models
# 预期返回: {"object":"list","data":[{"id":"你的模型名","object":"model"}]}
EvalScope 的 openai_api 模式本质上是一个 HTTP 客户端,向指定 URL 发送 Chat Completions 格式请求。它对模型服务的要求相对宽松:返回的 JSON 骨架(id、object、choices)完整即可,token 数填 0 不影响评测。
如果你的 910B 模型服务不是 OpenAI 格式,参考第六节适配层方案。
4.4 运行评测
两个脚本对应两类评测场景:
| 脚本 | 用途 | 数据集 |
|---|---|---|
run_eval.sh |
知识/推理/代码 | mmlu, ceval, gpqa_diamond, minerva_math, longbench_v2, humaneval, swe_bench_verified_mini |
run_agent_eval.sh |
Agent/工具调用 | bfcl_v4, tau_bench |
一、知识类(Knowledge)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS="mmlu ceval gpqa_diamond" \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
二、推理类(Math/reasoning)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS="minerva_math longbench_v2" \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
三、代码类(Coding)
# HumanEval(基础代码生成)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=humaneval \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
# SWE-bench(需要 Docker socket)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=swe_bench_verified_mini \
-e TIMEOUT=300 \
-e MAX_TOKENS=4096 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS="mmlu ceval gpqa_diamond" \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS="minerva_math longbench_v2" \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
三、代码类(Coding)
# HumanEval(基础代码生成)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=humaneval \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
# SWE-bench(需要 Docker socket)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=swe_bench_verified_mini \
-e TIMEOUT=300 \
-e MAX_TOKENS=4096 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
# HumanEval(基础代码生成)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=humaneval \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
# SWE-bench(需要 Docker socket)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=swe_bench_verified_mini \
-e TIMEOUT=300 \
-e MAX_TOKENS=4096 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
SWE-bench 比较特殊,它不仅评测代码生成,还要在 Docker 环境中实际运行测试用例来验证代码修复是否正确——所以必须挂载 /var/run/docker.sock,让容器内的 Docker CLI 连接宿主机 daemon 启动测试容器。TIMEOUT=300 是因为拉取/构建 Docker 镜像和运行测试比较耗时。
四、Agent/工具调用
# BFCL-v4(数据内置在镜像中,无需额外挂载)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=bfcl_v4 \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_agent_eval.sh
# tau_bench(需要用户模拟模型)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=tau_bench \
-e TAU_USER_MODEL=qwen3-next \
-e TAU_USER_API_BASE=http://10.7.6.39:3000/v1 \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_agent_eval.sh
# BFCL-v4(数据内置在镜像中,无需额外挂载)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=bfcl_v4 \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_agent_eval.sh
# tau_bench(需要用户模拟模型)
docker run --rm --network host \
-e MODEL=qwen3-next \
-e API_URL=http://10.7.6.39:3000/v1 \
-e DATASETS=tau_bench \
-e TAU_USER_MODEL=qwen3-next \
-e TAU_USER_API_BASE=http://10.7.6.39:3000/v1 \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
bash /workspace/scripts/run_agent_eval.sh
tau_bench 的工作原理是用一个"用户模拟模型"扮演客户角色,与被测模型进行多轮对话。这里 TAU_USER_MODEL 复用了被测模型本身——如果条件允许,用另一个独立的较小模型效果更好(模拟用户不需要很强)。
测试用小样本: -e LIMIT=10(加在以上任意命令中,正式跑时去掉即跑全量)。
BFCL subset 说明:
| 子集 | 含义 | 说明 |
|---|---|---|
simple_python/java/javascript |
单函数调用 | 基础 |
multiple |
多函数串行调用 | 中等 |
parallel |
并行多函数调用 | 中等 |
parallel_multiple |
并行+串行混用 | 较难 |
irrelevance |
无关函数干扰 | 考察判断力 |
multi_turn_* |
多轮对话 | Agent 能力 |
live_* |
需要真实 API 调用 | 离线跳过 |
memory_* |
需要向量记忆 | 需要 sentence-transformers,默认镜像未安装 |
4.5 直接 CLI 方式(不用脚本)
脚本方式等价于以下原生命令,方便理解参数含义:
docker run --rm --network host \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
-v /data/eval/reports:/workspace/reports \
evalscope-offline:arm64 \
evalscope eval \
--model qwen3-next \
--api-url http://10.7.6.39:3000/v1 \
--api-key ***"***\" \
--eval-type openai_api \
--datasets mmlu ceval \
--limit 10 \
--dataset-args '{\"mmlu\": {\"dataset_id\": \"/workspace/cache/modelscope/cais/mmlu\", \"few_shot_num\": 5}, \"ceval\": {\"dataset_id\": \"/workspace/cache/modelscope/evalscope/ceval\", \"few_shot_num\": 5}}' \
--generation-config '{\"temperature\":0,\"max_tokens\":2048,\"timeout\":180}'\n```\n\n**注意:** `dataset_id` 参数直接指定本地路径(`local_path` 已废弃),不需要 `--dataset-hub` 和 `--dataset-dir`。
## 五、OpenAI 标准接口格式要求
EvalScope `openai_api` 模式对模型服务的要求如下。
### 5.1 GET /v1/models
```json
{
"object": "list",
"data": [{"id": "your-model-name", "object": "model"}]
}
5.2 POST /v1/chat/completions
正常返回:
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"model": "your-model-name",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "回答内容"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150}
}
工具调用返回(BFCL 场景,content 为 null):
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Beijing\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}
usage 字段的 token 数填 0 不影响评测结果,但 id、object、choices 骨架必须完整。
六、非标准接口适配方案
如果 910B 模型服务不是 OpenAI 格式,用 FastAPI 写转换层:
from fastapi import FastAPI
import httpx
import uuid
app = FastAPI()
YOUR_ENDPOINT = "http://127.0.0.1:9090/generate"
@app.get("/v1/models")
async def models():
return {"object": "list", "data": [{"id": "my-model", "object": "model"}]}
@app.post("/v1/chat/completions")
async def chat(req: dict):
messages = req.get("messages", [])
tools = req.get("tools")
prompt = messages[-1].get("content", "") if messages else ""
if messages and messages[0].get("role") == "system":
prompt = messages[0]["content"] + "\n\n" + prompt
if tools:
tool_desc = "\n".join(
f"Function: {t['function']['name']}\n{t['function']['description']}"
for t in tools
)
prompt = f"{tool_desc}\n\nUser request: {prompt}\n\nRespond with function call JSON."
payload = {
"prompt": prompt,
"max_new_tokens": req.get("max_tokens", 2048),
"temperature": req.get("temperature", 0),
}
async with httpx.AsyncClient() as client:
resp = await client.post(YOUR_ENDPOINT, json=payload, timeout=300)
result = resp.json()
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:12]}",
"object": "chat.completion",
"model": req["model"],
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": result["text"]},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
}
启动:
pip install fastapi uvicorn httpx
uvicorn adapter:app --host 0.0.0.0 --port 8000
然后 EvalScope 指向 http://127.0.0.1:8000/v1。
七、冒烟验证
在正式跑评测之前,建议先用内置的 mock API 验证整条链路是否打通——镜像是否正确加载、数据集挂载是否生效、EvalScope 是否能正常调度。
思路很简单:启动一个 mock API(不管问什么都返回固定答案),然后跑一条 HumanEval 样本。如果能正常跑完(Benchmark finished),说明链路通了;如果卡在数据集加载阶段,说明挂载问题。
# 终端1:启动 mock API
docker run --rm -p 18080:18080 evalscope-offline:arm64 \
python /workspace/scripts/run_smoke_mock_api.py --port 18080
# 终端2:冒烟测试
docker run --rm --network host \
-e MODEL=mock \
-e API_URL=http://127.0.0.1:18080/v1 \
-e DATASETS=humaneval \
-e LIMIT=1 \
-v /data/eval/modelscope_cache:/workspace/cache/modelscope \
evalscope-offline:arm64 \
bash /workspace/scripts/run_eval.sh
看到 Benchmark humaneval evaluation finished 即链路正常。
八、踩坑记录
实际部署中遇到的几个问题及解决方案。
8.1 dataset_dir 路径错误
现象:evalscope 报 Running[eval] 0%| 0/0,0 条 benchmark,输出 DummyCustomModel。
原因:--dataset-dir 设置了错误的路径 /workspace/cache/modelscope/datasets。ModelScope 的 snapshot_download(cache_dir=cache) 将数据集下载到 {cache}/{org}/{repo}/ 结构,而非 {cache}/datasets/{org}/{repo}/。加 /datasets 后缀反而让路径不对。
修正:直接不用 --dataset-hub 和 --dataset-dir,改用 dataset_id 指定绝对路径。
8.2 不要用 :ro 只读挂载
EvalScope 运行时会往数据集目录写入缓存索引文件,只读挂载会导致运行失败。所有 -v 挂载命令中请去掉 :ro。
这个坑比较隐蔽——直觉上数据集是只读的,但实际上 Arrow/Parquet 格式的数据集首次加载时框架会生成索引缓存,写在数据集目录本身。如果确实不想让数据集目录被修改,可以 cp -r 一份副本到可写位置再挂载。
8.3 evalscope 命令行规范
官方只使用 evalscope eval + CLI 参数。--config 不是 eval 子命令的参数,用了会报 unrecognized arguments。
--eval-config 用于非 Native 后端(OpenCompass/VLMEvalKit)的配置文件路径,不是任务配置。如果你在 YAML 里写了 datasets、model 等字段,然后通过 --eval-config 传入,这些字段会被当成后端配置的一部分存储,顶层的 datasets 仍然为空 → 0 条 benchmark。
8.4 推荐用法:dataset_id 指定本地路径
官方文档推荐用 --dataset-args 的 dataset_id 参数直接指定本地路径。旧版的 local_path 参数已废弃。
--dataset-args '{"mmlu": {"dataset_id": "/path/to/data", "few_shot_num": 5}}'
这种方式完全离线、不经过 ModelScope Hub 缓存机制,避免了路径匹配失败和自动联网下载的问题。
8.5 BFCL-v4 数据来源确认
关于 BFCL 数据到底需不需要额外下载,走了一段弯路:
- 最初认为"内置在 bfcl-eval 包中"不需要额外处理
- 后尝试从 HuggingFace 预下载,发现 BFCL 数据集是 gated repo(需要 HF token),所有 candidate repo ID 都返回 404/401
- 最终通过 Docker 容器实际运行 evalscope eval 验证,
bfcl-eval包内data/目录含 12MB JSON 测试数据,全部 18 个 subset 覆盖
所以 BFCL-v4 评测完全离线可用。如果未来 evalscope 升级改变了数据加载方式,可能需要重新评估。
8.6 数据集必须全部传输
NPU 节点上如果只传了部分数据集(比如只有 humaneval),evalscope 检测到其他数据集缺失会尝试从 modelscope.cn 联网下载 → DNS 解析失败 → NameResolutionError。确保所有在 DATASETS 中列出的数据集都已 scp 到 NPU。
九、注意事项
- 数据集不在镜像里:必须
-v挂载,否则 evalscope 尝试联网下载而失败 - 不要用
:ro:挂载不加:ro,EvalScope 运行时会写缓存 - SWE-bench 需要 Docker socket:
-v /var/run/docker.sock:/var/run/docker.sock - BFCL 离线可用:数据内置在 bfcl-eval 包中
- torch/NVIDIA 包:镜像内是 ARM64 CPU 版 torch,评测走 HTTP API 不依赖 GPU
- 内存建议:10GB 镜像 + 数据集加载 + 模型推理,建议 910B >= 32GB 内存
- 正式跑去掉
LIMIT:不设此环境变量即跑全量
简记。
十、构建链路踩坑补记(EvalScope 1.8/1.9 + 国内网络)
前面几节基于 EvalScope 1.7.1。升级到 1.8/1.9 后,依赖链有变化,加上国内网络环境,重建镜像时又踩了一串坑。本节把完整的构建链路修复记录下来,作为补充。
10.1 pip 直连 pypi.org 被 TLS 掐断 → 换清华源
现象:构建卡在 pip 装包,反复重试后失败:
WARNING: Retrying ... SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol'))
ERROR: Could not find a version that satisfies the requirement evalscope[swe-bench]>=1.8.0 (from versions: none)
ERROR: No matching distribution found for evalscope[swe-bench]>=1.8.0
原因:容器内直连 pypi.org 的 TLS 连接被中途重置,五次重试全挂。
修正:Dockerfile 里装包前先配置清华镜像源。pip config set 会写入 /root/.config/pip/pip.conf,对后续所有 RUN 的 pip 都生效(包括单独装 bfcl、tau-bench 那几步)。
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \
pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn && \
pip install --upgrade pip && pip install -r /workspace/requirements.txt
10.2 datasets 版本冲突(ResolutionImpossible,别被 fsspec 带偏)
现象:换上镜像源后,pip 能连了,但报 ResolutionImpossible,错误信息指向 fsspec:
The conflict is caused by:
datasets 2.18.0 depends on fsspec<=2024.2.0 and >=2023.1.0
fsspec[http] 2024.2.0 depends on fsspec 2024.2.0
ERROR: ResolutionImpossible
真因:跟 fsspec 没关系。evalscope[swe-bench] 依赖 modelscope[datasets]>=1.34,而 modelscope 的 datasets extra 硬性要求 datasets>=4.0.0;但 requirements 里 pin 的是 datasets<3.0.0——两者根本无解。pip 为了满足 <3.0.0,把 modelscope 从 1.38 一路回溯到 1.34,最后在 fsspec 版本上撞墙报错,错误信息极具误导性。
排查技巧:与其翻 pip 那一大堆回溯日志,不如直接查依赖元数据,一眼定位:
curl -s https://pypi.org/pypi/modelscope/json | python3 -c \
"import sys,json;[print(r) for r in json.load(sys.stdin)['info']['requires_dist'] if 'datasets' in r.lower()]"
# → datasets<=4.8.4,>=4.0.0; extra == "datasets"
修正:requirements.txt 把 datasets 放开到 modelscope 要求的区间(swebench 4.1.0 只写 datasets 无上限,兼容 4.x):
datasets>=4.0.0,<=4.8.4
10.3 tau-bench 的 GitHub 安装在国内不可靠 → vendored 进上下文
现象:镜像内 pip install 前的 git clone,等 127 秒后失败:
Cloning into '/workspace/tau-bench'...
fatal: unable to access 'https://github.com/sierra-research/tau-bench/':
GnuTLS recv error (-110): The TLS connection was non-properly terminated.
GitHub 直连被掐。
修正:这本来就是"离线镜像",索性把 tau-bench 直接 vendored 进构建上下文——宿主机用 GitHub 加速镜像克隆好,去掉 .git,Dockerfile 改成 COPY:
# 宿主机(ghfast.top 是可用的 GitHub 加速镜像)
git clone --depth 1 https://ghfast.top/https://github.com/sierra-research/tau-bench tau-bench
rm -rf tau-bench/.git
# Dockerfile:由 git clone 改为 COPY(tau-bench 的 pip 依赖仍走清华源,正常)
COPY tau-bench /workspace/tau-bench
RUN pip install -e /workspace/tau-bench
注意:tau-bench 若在工作区是 git 未跟踪目录,容易被一次 git clean 清掉,构建时报 COPY ... "/tau-bench": not found,重新 clone 一份即可。
10.4 arm64 交叉构建实测(Intel Mac + QEMU)
docker buildx build --platform linux/arm64 \
-f Dockerfile.arm64 -t evalscope-offline:arm64 --load .
- 前提:QEMU binfmt 已注册,
docker run --rm --platform linux/arm64 alpine uname -m 返回 aarch64 即可。
- 耗时:Intel Mac 上全程 QEMU 模拟,约 50 分钟(下载 + 编译 sdist + 装 torch/CUDA 都在模拟层,慢)。
- 关键结论(纠正一个常见误解):arm64 上的 torch 并不是 CPU 版,它照样拉整套 NVIDIA CUDA 依赖——但这些包都有 aarch64 的 wheel(
nvidia_cublas-…-manylinux_2_27_aarch64.whl 等),所以不会出现"x86-only CUDA wheel 导致 arm64 构建失败"的情况。最终装的是 torch 2.12.1+cu130。
docker buildx build --platform linux/arm64 \
-f Dockerfile.arm64 -t evalscope-offline:arm64 --load .
docker run --rm --platform linux/arm64 alpine uname -m 返回 aarch64 即可。nvidia_cublas-…-manylinux_2_27_aarch64.whl 等),所以不会出现"x86-only CUDA wheel 导致 arm64 构建失败"的情况。最终装的是 torch 2.12.1+cu130。验证:
docker image inspect evalscope-offline:arm64 --format '{{.Architecture}}' # arm64
docker run --rm --platform linux/arm64 evalscope-offline:arm64 python -c \
"import platform,evalscope,datasets,torch; \
print(platform.machine(), evalscope.__version__, datasets.__version__, torch.__version__)"
# aarch64 1.9.0 4.8.4 2.12.1+cu130
镜像 6.8GB,容器内 machine=aarch64,evalscope / datasets / swebench / bfcl_eval / tau_bench / torch 全部 import 正常。
10.5 Docker Desktop 环境层面的两个坑
- 引擎假死:
docker 客户端正常,但 daemon 返回 500 ... /_ping,日志里 apiproxy 反复 dialing 192.168.65.7:2375 ... no route to host——VM 网络挂了。彻底退出并杀干净进程再重启可恢复:
osascript -e 'quit app "Docker"'
pkill -f com.docker.backend; pkill -f "Docker Desktop"; pkill -f com.docker.virtualization
open -a Docker
- 拉基础镜像失败:
connecting to 127.0.0.1:18888: connection refused——Docker 配了本地代理(Clash 之类),代理没开时任何 pull 都失败。要么开代理,要么在 Docker Desktop → Settings → Resources → Proxies 里关掉。另外 ~/.docker/daemon.json 的 registry-mirrors 别塞占位符(比如 https://your_preferred_mirror)和一堆失效/重复地址,会拖慢并偶发失败。
- 小提示:macOS 自带没有
timeout 命令(那是 coreutils 的 gtimeout),写等待/超时脚本时容易踩空。
docker 客户端正常,但 daemon 返回 500 ... /_ping,日志里 apiproxy 反复 dialing 192.168.65.7:2375 ... no route to host——VM 网络挂了。彻底退出并杀干净进程再重启可恢复:osascript -e 'quit app "Docker"'
pkill -f com.docker.backend; pkill -f "Docker Desktop"; pkill -f com.docker.virtualization
open -a Docker
connecting to 127.0.0.1:18888: connection refused——Docker 配了本地代理(Clash 之类),代理没开时任何 pull 都失败。要么开代理,要么在 Docker Desktop → Settings → Resources → Proxies 里关掉。另外 ~/.docker/daemon.json 的 registry-mirrors 别塞占位符(比如 https://your_preferred_mirror)和一堆失效/重复地址,会拖慢并偶发失败。timeout 命令(那是 coreutils 的 gtimeout),写等待/超时脚本时容易踩空。补记完毕。
10.6 冒烟测试中发现的额外坑
镜像构建完成后,在 Intel Mac 上用 QEMU 模拟跑了一次完整的冒烟验证(mock API + 全部 benchmark),又踩出三个坑,均已在项目和 Dockerfile 中修复。
坑一:verify_offline.sh 自带 :ro 只读挂载
项目自带的 verify_offline.sh 把 modelscope_cache 挂成了 :ro(只读)。但 evalscope 1.9.0 加载 mmlu 时会尝试删除 dataset_infos.json 文件——只读挂载直接 OSError: Read-only file system ——完全复现了第八节博文里"不要用 :ro"的那条。scripts/verify_offline.sh 已去掉 :ro。
坑二:自定义 DNS 导致 host.docker.internal 解析失败
~/ .docker/daemon.json 自定义了 dns: [114.114.114.114, 8.8.8.8],覆盖了 Docker Desktop 内置的 DNS 解析,导致容器里 host.docker.internal → Name or service not known → mock API 连不上 → APIConnectionError。修复:给 eval 容器的 docker run 加 --add-host=host.docker.internal:host-gateway(直接写 /etc/hosts,绕过 DNS)。verify_offline.sh 已添加此参数。
坑三:bfcl-eval 缺少 soundfile 依赖
BFCL-v4 的推理阶段能跑通,但进入评分阶段时报 ModuleNotFoundError: No module named 'soundfile'。链路是 bfcl_eval 评分 → qwen_agent → soundfile,这个依赖在 bfcl-eval==2025.10.27.1 的元数据里是间接的(通过 qwen_agent),所以 evalscope[bfcl] 安装时不会自动拉进来。
好消息是 PySoundFile 0.14+ 的 aarch64 wheel 自带 libsndfile,不需要 apt 装系统库——只需 pip install soundfile 即可。Dockerfile 尾部追加了一个 RUN pip install soundfile 层(放最后不影响重型缓存)。
此外,scripts/run_agent_eval.sh 的 build_dataset_args 函数有个 JSON 构造 bug:把已完整的 JSON 对象又套了一层大括号,产生 {{...}} 非法 JSON → JSONDecodeError。已用 Python 合并替代字符串拼接,脚本中也已修复。
全文补记完毕。
10.7 datasets 4.0.0 Value dtype 兼容补丁(2026-07-13)
现象:SWE-bench 评测在加载 oracle 数据集时报错:
TypeError: Value.__init__() missing 1 required positional argument: 'dtype'
完整调用栈:
File "datasets/info.py", line 170, in __post_init__
self.features = Features.from_dict(self.features)
File "datasets/features/features.py", line 1583, in generate_from_dict
return class_type(**{k: v for k, v in obj.items() if k in field_names})
TypeError: Value.__init__() missing 1 required positional argument: 'dtype'
原因:这其实是 10.2 节修复 ResolutionImpossible 的副作用。为了满足 modelscope[datasets]>=1.34 的要求,我们把 datasets 从 <3.0.0 升到了 >=4.0.0。但 datasets 4.0.0 重做了 Value feature 类的 __init__,把 dtype 改成了必传位置参数。而 ModelScope 上缓存的旧数据集(如 princeton-nlp/SWE-bench_oracle)是用旧版 datasets 序列化的,元数据里没有 dtype 字段。新 datasets 加载旧元数据时直接炸。
这个错误第一次出现在 7 月 7 日——当时忙着修 ResolutionImpossible 和 tau-bench vendored,没注意到 datasets 版本升级的连带影响。7 月 13 日重新跑 SWE-bench 时才正式确认并修复。
修正:写了一个 monkey-patch 脚本 patch_datasets_compat.py,拦截 datasets.features.features.generate_from_dict。当遇到旧格式的 Value 对象(_type=Value 但缺少 dtype)时,自动补 dtype: "string" 作为默认值:
import datasets.features.features as f
_orig = f.generate_from_dict
def _patched(obj):
if isinstance(obj, dict) and obj.get("_type") == "Value" and "dtype" not in obj:
obj = {**obj, "dtype": "string"}
return _orig(obj)
f.generate_from_dict = _patched
为了让补丁在任何 Python 进程启动时自动生效,在 Dockerfile 中通过 install_patch.py 将补丁脚本安装到 site-packages/,并创建 .pth 文件触发自动 import:
# install_patch.py
import shutil, site, os
src = '/workspace/scripts/patch_datasets_compat.py'
dst = os.path.join(site.getsitepackages()[0], 'patch_datasets_compat.py')
shutil.copy(src, dst)
with open(os.path.join(site.getsitepackages()[0], 'patch_datasets_compat.pth'), 'w') as f:
f.write('import patch_datasets_compat\n')
.pth 文件是 Python 的 site 模块机制——放在 site-packages 下的 .pth 文件会被 Python 启动时自动处理,以 import 开头的行会被执行,等价于在 sitecustomize.py 中 import。
验证:构建后在容器内跑:
from datasets.features.features import generate_from_dict
r = generate_from_dict({'_type': 'Value', 'id': None})
print(r) # → Value('string')
旧格式反序列化正常,不再报 TypeError。
为什么不用更简单的方法? 考虑过三个方案:
- 清理 modelscope 缓存重新下载:不行。910B 是离线环境,重新下载要走 Mac → scp 流程,而且没法保证新下载的元数据格式兼容——datasets 4.x 写入的是新格式,但源数据集是旧格式存储的,问题依然存在。
- 降级 datasets:不行。modelscope>=1.34 硬性要求 datasets>=4.0.0。
- 直接改 datasets 源码:可行但脆弱,pip 升级会覆盖。
.pth自动注入更优雅,不侵入三方包。
至此,EvalScope 1.9.0 + 离线 arm64 镜像的所有构建和部署问题已全部修复。
10.8 Dockerfile 行内 Python 多行语法陷阱
写补丁安装的时候又踩了个 Dockerfile 语法坑:
# ❌ 错误:Docker 把第二个 import 当成了一条新指令
RUN python3 -c "
import shutil, site, os
...
"
# → dockerfile parse error: unknown instruction: import
Dockerfile 的 RUN 命令不支持跨行字符串(除非用 \ 连接或 HEREDOC 语法)。修正方法:把 Python 逻辑抽到独立脚本 install_patch.py 里,Dockerfile 只跑一条 RUN python3 /workspace/scripts/install_patch.py。
完整补记完毕。








COMMENTS | NOTHING