EvalScope 离线评测部署简记(华为910B ARM + OpenAI 兼容接口)

发表于 2026-06-05  227 次阅读


文章目录

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),无需额外下载。

思路

整个离线方案要解决三个核心矛盾:

  1. 断网 vs 数据集下载:数据集在 ModelScope/HuggingFace 上,NPU 节点无外网。思路是在 Mac 上预先下载,打包后 scp 传输,通过 Volume 挂载进容器。
  2. ARM64 vs x86 Mac:910B 是 ARM64,Mac 是 Intel x86。用 Docker Buildx 交叉编译 ARM64 镜像,这样容器内的 Python 环境(Python 3.10 + ARM64 依赖)和 NPU 原生环境一致。
  3. 多种数据集 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_numsubset_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"]

几个设计考量: - 选 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] 是 EvalScope 的 extras 安装方式——swe-bench 附加 SWE-bench 适配器,bfcl 附加 BFCL 适配器。

1.4 一键构建脚本

cd evalscope_offline
bash scripts/download_assets.sh

这个脚本按顺序执行:

  1. 下载 Python wheels 到 wheels/(可选,Mac ARM64 交叉下载可能失败,此时跳过不影响后续)
  2. 下载 ModelScope 数据集到 modelscope_cache/:调用 ModelScope snapshot_download 下载全部 7 个数据集,约 5-10 分钟(取决于网络)
  3. 构建 ARM64 Docker 镜像docker buildx build --platform linux/arm64 -t evalscope-offline:arm64 . --load,约 15-30 分钟(取决于网络和 CPU)
  4. 导出镜像为 tar.gzdocker 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/

三、覆盖维度与数据集

维度 数据集 样本量 评测指标 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/               # 评测脚本

为什么要 --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 骨架(idobjectchoices)完整即可,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

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

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 不影响评测结果,但 idobjectchoices 骨架必须完整。

六、非标准接口适配方案

如果 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 里写了 datasetsmodel 等字段,然后通过 --eval-config 传入,这些字段会被当成后端配置的一部分存储,顶层的 datasets 仍然为空 → 0 条 benchmark。

8.4 推荐用法:dataset_id 指定本地路径

官方文档推荐用 --dataset-argsdataset_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。

九、注意事项

  1. 数据集不在镜像里:必须 -v 挂载,否则 evalscope 尝试联网下载而失败
  2. 不要用 :ro:挂载不加 :ro,EvalScope 运行时会写缓存
  3. SWE-bench 需要 Docker socket-v /var/run/docker.sock:/var/run/docker.sock
  4. BFCL 离线可用:数据内置在 bfcl-eval 包中
  5. torch/NVIDIA 包:镜像内是 ARM64 CPU 版 torch,评测走 HTTP API 不依赖 GPU
  6. 内存建议:10GB 镜像 + 数据集加载 + 模型推理,建议 910B >= 32GB 内存
  7. 正式跑去掉 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 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.jsonregistry-mirrors 别塞占位符(比如 https://your_preferred_mirror)和一堆失效/重复地址,会拖慢并偶发失败。
  • 小提示:macOS 自带没有 timeout 命令(那是 coreutils 的 gtimeout),写等待/超时脚本时容易踩空。

补记完毕。

10.6 冒烟测试中发现的额外坑

镜像构建完成后,在 Intel Mac 上用 QEMU 模拟跑了一次完整的冒烟验证(mock API + 全部 benchmark),又踩出三个坑,均已在项目和 Dockerfile 中修复。

坑一:verify_offline.sh 自带 :ro 只读挂载

项目自带的 verify_offline.shmodelscope_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.internalName 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_agentsoundfile,这个依赖在 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.shbuild_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

完整补记完毕。

本站文章基于国际协议BY-NA-SA 4.0协议共享;
如未特殊说明,本站文章皆为原创文章,请规范转载。

0

scanz个人博客