> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuneplane.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 离线内网部署

> 在没有 Hugging Face、PyPI 和公共镜像仓库出口的网络里跑 TunePlane。

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
TUNEPLANE_HF_ENDPOINT=https://mirrors.your-company.com/repository/huggingface
```

这一项设置就是权重下载能在镜像后面工作的原因。本页剩下的内容是另外三样会坏掉的东西，以及各自怎么办。

## 模型权重

`huggingface_hub` 读 `HF_ENDPOINT`，而平台会把 `TUNEPLANE_HF_ENDPOINT` 原样注入每个训练容器和
Playground 容器。把它指向一个反向代理即可——Nexus、Artifactory，或者任何能代理 Hub 的东西。

有两项调整会自动跟着一起生效，它们的存在都是因为某种极难诊断的失败：

<AccordionGroup>
  <Accordion title="超时被放宽" icon="clock">
    `huggingface_hub` 默认十秒超时。镜像第一次取一个它从未缓存过的文件时要一路回源，
    这经常超过十秒。症状是冷文件下载失败、重试又成功，看起来像网络不稳定，而不像配置问题。
  </Accordion>

  <Accordion title="Xet 被关闭" icon="ban">
    Xet 需要短时效 token，而反向代理签不出来。不关的话，权重下载会卡在一个 400 上，
    而那个响应体里除了一个 URL 什么都没有——没有消息、没有字段，连搜都没得搜。

    如果你的镜像确实实现了 Xet，用 `TUNEPLANE_PASSTHROUGH_ENV` 显式覆盖。
  </Accordion>
</AccordionGroup>

<Warning>
  拉不到权重的作业不会快速失败。它会启动、打印下载日志，然后一直卡到超时——
  整个过程都占着它的 GPU 分配。在向团队开放提交之前，先确认镜像是通的。
</Warning>

## 容器镜像

训练镜像来自 recipe catalog，而 catalog 里写的是 `nvcr.io` 这类公共仓库。内网里需要先转存再重定向：

| 设置                                   | 作用                                             |
| ------------------------------------ | ---------------------------------------------- |
| `TUNEPLANE_RUNTIME_REGISTRY_FILE`    | 把 `runtime_id` 映射到你的仓库里实际存在的工件，覆盖 catalog 里的地址 |
| `TUNEPLANE_ALLOWED_IMAGE_REGISTRIES` | 用户用 `--image` 时允许的主机，设成你的内网仓库                  |
| `TUNEPLANE_K8S_IMAGE_PULL_SECRET`    | KubeRay 下的拉取凭据                                 |

生产环境用 digest 而不是 tag。在你自己的镜像仓库里挪动的 tag，仍然是一个挪动过的 tag。

## 在镜像站后面构建镜像

Dockerfile 默认走公共上游，所以在哪都能构建。用 build 参数把它指向内网镜像站：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
docker build \
  --build-arg APT_MIRROR=https://mirrors.your-company.com/repository \
  --build-arg PYPI_INDEX=https://mirrors.your-company.com/repository/pypi-group/simple/ \
  --build-arg NPM_REGISTRY=https://mirrors.your-company.com/repository/npm-group/ \
  --build-arg UV_IMAGE=mirror.your-company.com/astral-sh/uv:python3.13-trixie-slim \
  -t tuneplane-server:0.3.0 .
```

`APT_MIRROR` 留空就保持 Debian 自己的源——能正常访问互联网的机器上你要的就是这个。

## 平台运行时

作业侧需要 `tuneplane` 才能回传指标，而训练镜像里不会有它。

默认的 `TUNEPLANE_JOB_RUNNER_MODE=bundled` 不需要任何网络就解决了这件事：
服务端把一个内容寻址的 PEX 注入作业，训练镜像不需要预装任何东西。
这是少数几个「离线反而更简单」的地方——这项设置保持默认就好。

## 数据集

这里没有任何东西需要访问互联网。数据集用 `tp dataset push` 推进部署自己的对象存储，
作业启动时拉进共享缓存。见[数据集](/zh-Hans/guides/datasets)。

## 基准数据

基准会自己拉数据，三个 harness 各拉各的地方。其中两个需要你做点事。

**lm-eval**（gsm8k、mmlu、humaneval、ifeval、math）从 Hub 下载，上面的 `TUNEPLANE_HF_ENDPOINT`
已经覆盖，不用额外做什么。

**evalscope**（C-Eval 及其它中文基准）是 ModelScope 原生的：它从 `www.modelscope.cn` 下载
`evalscope/ceval`。它的覆盖变量是
[`MODELSCOPE_DOMAIN`](https://github.com/modelscope/modelscope)，而它和 `HF_ENDPOINT` 有一处关键
差别，直接决定了你的拓扑：它接受的是**域名，不是 URL**。客户端只做 `https://` + 这个值，不带路径。
Nexus 的代理仓库在 `/repository/<名字>` 下，因此没法直接填 —— 镜像必须在一个属于它自己的主机名
根路径上应答：

```nginx theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
server {
  server_name modelscope.mirrors.your-company.com;
  location / { proxy_pass https://mirrors.your-company.com/repository/modelscope/; }
}
```

然后透传给作业。平台侧不用改任何代码，这个透传就是干这个的：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
TUNEPLANE_PASSTHROUGH_ENV='{"MODELSCOPE_DOMAIN": "modelscope.mirrors.your-company.com"}'
```

把 evalscope 切到 Hugging Face hub 是**行不通**的，而且失败方式有误导性：`--dataset-hub huggingface`
只改去哪找、不翻译 id，于是向一个从来没有 `evalscope/ceval` 的镜像去要它，报错写的是「数据集找不到」，
而不是「连不上 ModelScope」。

**有些 suite 拉的是固定 URL。** 比如 lm-eval 的 `math500` 从
`openaipublic.blob.core.windows.net` 读一个 CSV。这类没有端点设置可用：要么为那个主机开一个 raw
代理，要么接受这个 suite 不可用。在向别人承诺某个基准之前，值得按 suite 逐个确认一遍。

## 确认成功

提交快速开始里那个作业，看日志。你要看到的顺序是：

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
resolving Qwen/Qwen2.5-1.5B via https://mirrors.your-company.com/repository/huggingface
downloading model-00001-of-00002.safetensors
...
Ray runtime started
```

卡在第一行和第二行之间是镜像的问题。响应体里只有一个裸 URL 的 400 是 Xet。
大文件在大约十秒后超时，说明放宽的超时没有生效——检查设置 `HF_ENDPOINT` 的是平台，而不是你自己的脚本。
