跳转到内容

在 GPU 服务器、SLURM 集群或 NAT 后的机器上训练

把 Potato 的训练运行发送到 ssh GPU 服务器、SLURM 计算节点或轮询代理,附每种后端的配置、检查命令和常见故障。

v2.14.0 新增

本教程把 Potato 的模型训练从标注服务器移到带 GPU 的机器上。教程写给这样的研究管理员:标注服务器是一台小型云虚拟机或一台笔记本,而 GPU 在实验室服务器、大学集群,或服务器无法连接的工作站上。对于三种远程后端,本页依次介绍何时选择该后端、远程机器需要安装什么、配置、检查命令、运行期间发生什么、如何取消运行,以及你最可能遇到的故障。教程假定你已有一个能在标注服务器上训练的项目,例如训练你的第一个模型中的评论项目。

运行可以在四个地方执行

后端是训练运行执行的地方。Potato 始终有 local 后端,即标注服务器上的一个子进程。其他后端在 model_training.backends 下命名:

类型训练器在哪里运行谁启动远程一侧模型文件最终在哪里
local标注服务器,在子进程中Potato该次运行的 artifacts/ 目录
sshPotato 能通过 ssh 到达的机器,例如实验室 GPU 服务器Potato,通过 ssh复制回 artifacts/
slurm集群计算节点,作为批处理作业Potato,通过 ssh 连到登录节点,再 sbatch复制回 artifacts/
agentPotato 无法到达的机器,例如 NAT 后的工作站你,用 potato training agent在训练机器上

当标注服务器能用 ssh 密钥登录 GPU 机器时,选择 ssh。当 GPU 位于 SLURM 调度器之后,而你能登录的机器是登录节点时,选择 slurm。当服务器无法向 GPU 机器发起任何连接时,选择 agent,因为所有连接都由代理自己建立。家用路由器或校园防火墙后的工作站是使用代理的常见情形。无论哪种后端,预测和该次运行的结果都会到达服务器,所以不管训练器在哪里运行,回写和留出得分的工作方式都相同。除 decision-calibrate 外,每个训练器都可以在任何后端上运行。

项目配置

下面的配置是第一个教程中的评论项目,加上了全部三种远程后端。真实项目只需要你用到的后端,每种后端在下文各有一节。max_wall_seconds: 3600 会停止任何一小时内没有完成的运行。在 ssh 或 agent 后端上,服务器每秒检查一次该限制,所以在远程机器上卡住的运行占用运行队列的时间不会超过这个限制:

yaml
annotation_task_name: "Review Sentiment"
task_dir: "."
data_files:
  - "data/reviews.json"
item_properties:
  id_key: "id"
  text_key: "text"
output_annotation_dir: "annotation_output"
admin_api_key: "change-me"
 
annotation_schemes:
  - annotation_type: "radio"
    name: "sentiment"
    description: "Is this review positive or negative?"
    labels: ["positive", "negative"]
 
model_training:
  enabled: true
  backend: gpu-server        # the default for runs; leave out to keep runs local
  max_wall_seconds: 3600
  writeback:
    schemas: ["sentiment"]
    include_unlabeled: true
    min_confidence: 0.6
  backends:
    gpu-server:
      type: ssh
      host: gpu.example.org
      python: /opt/conda/bin/python3
      gpus: auto
      min_free_gpu_mb: 4000
    cluster:
      type: slurm
      host: login.cluster.example.org
      python: /opt/conda/bin/python3
      job_python: /shared/envs/train/bin/python
      remote_dir: /shared/projects/me/potato-training
      gres: gpu:1
      time: "04:00:00"
      prelude: ["module load cuda/12.4"]
    lab-workstation:
      type: agent
      queue_timeout_s: 3600

启动服务器之前先验证配置:

bash
potato validate config.yaml --strict

验证器会报告后端类型不接受的键、没有 host 的 ssh 或 slurm 后端、位于 /tmp 下的 SLURM remote_dir,以及指向不存在的后端的 model_training.backend。遇到同样的错误,服务器也不会启动,所以拼错的 pyhton 会在启动时让服务器停下,而不是把运行送到默认的 python3。

Runs on 菜单和 API

/admin/training 的训练页面有一个 Runs on 菜单,列出每个后端及其类型和主机,例如 gpu-server (ssh to gpu.example.org)。model_training.backend 决定菜单最先选中哪一项,Runs 表则在 Ran on 列中记录每次运行在哪里执行。通过 API 启动的运行,在请求体的 backend 字段中指明它的后端:

bash
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  http://localhost:8000/admin/training/api/runs \
  -d '{"trainer": "hf-text-classification", "schemas": ["sentiment"], "backend": "cluster"}'

配置中不存在的后端名称会被以 409 拒绝。自动重新训练使用 model_training.backend,除非 auto_retrain.backend 指定了另一个后端,auto_retrain 的全部内容见循环中的模型。无论哪种后端,同一时间只执行一次运行,在另一次运行进行期间启动的运行会在队列中等待。

在本机训练

在把运行移到别处之前,先看一下本地运行会用到哪个设备。使用默认的 device: auto 时,本地运行先用 CUDA,再用 Apple 的 Metal(mps),最后用 CPU,而 device: cuda:1 则指定一块 GPU。检查命令会打印本地运行将使用的设备:

bash
potato training check local config.yaml

在 Apple silicon 的 Mac 上,该命令会打印 local: this machine, Apple Metal (mps)。你所要求的设备如果不存在,会退回到 CPU 或另一个加速器,并在运行日志中给出警告。当结果是 CPU,而训练器要微调 transformer 时,请把运行移到下面的某个远程后端。配置列出了本地运行的 device、num_threads 和 max_ram_mb。

通过 ssh 连接的 GPU 服务器

ssh 后端在标注服务器能通过 ssh 到达的机器上运行训练器,通常是实验室 GPU 服务器。Potato 自己启动远程一侧的一切,所以除 Python 和训练器的包之外,无需手工在那里安装或启动任何东西。GPU 主机无需安装 Potato,因为 Potato 会在第一次运行时把自己的 Python 源码复制到主机上,并在源码改变之前一直复用该副本。

ssh 后端的要求

标注服务器所用的账户需要一个能无提示登录 GPU 主机的 ssh 密钥。Potato 用 BatchMode=yes 运行 ssh,所以出现密码提示时运行会立即失败,而不会卡住。以该账户运行 ssh gpu.example.org true 必须成功,带口令的密钥在 ssh-agent 持有该密钥时可用。host 的值可以是 ssh 接受的任何名称,~/.ssh/config 中的别名也可以。user、port 和 identity_file 这几个键会分别以 -l、-p 和 -i 传给 ssh。

GPU 主机需要 Python 3.8 或更新版本,以及你所运行训练器的包。对 hf-text-classification 而言,这些包是 torch、transformers 和 peft,即 potato-annotation[train-text] 这个 extra 的内容。训练器页面写明了每个训练器的安装方式。当主机的解释器缺少某个包时,可以把 python 指向一个有该包的解释器,或把该包列在 packages 下。设置了 packages 后,Potato 会在 remote_dir 下用 --system-site-packages 创建一个虚拟环境,并把列出的包一次性装进该环境。主机自带的 CUDA 版 torch 在环境里仍然可见:

yaml
model_training:
  enabled: true
  backends:
    gpu-server:
      type: ssh
      host: gpu.example.org
      python: /opt/conda/bin/python3
      packages: ["transformers>=4.46.0", "peft>=0.13.0"]
      gpus: auto
      min_free_gpu_mb: 4000
      num_threads: 8

使用 gpus: auto 时,每次运行选取空闲显存最多的 GPU,当没有 GPU 有 min_free_gpu_mb(默认 2000)的空闲显存时拒绝启动。Potato 比较的是空闲显存而不是利用率,因为在共享服务器上,利用率低的显卡也可能所剩显存无几。请把 min_free_gpu_mb 设在接近训练器所需显存的位置。gpus: "1" 固定使用 1 号 GPU,"0,1" 固定使用两块 GPU,gpus: none 则在 CPU 上训练。完全没有 GPU 的主机在 auto 下也会在 CPU 上训练。num_threads 为该次运行设置 OMP_NUM_THREADS、MKL_NUM_THREADS 和 OPENBLAS_NUM_THREADS。有些共享服务器在登录 shell 中设置 OMP_NUM_THREADS=1,而 num_threads 会覆盖登录 shell 的值。

ssh 后端的检查命令

在标注服务器上,以服务器所用的账户运行检查:

bash
potato training check gpu-server config.yaml

该命令会连接并报告远程 Python 版本、每块 GPU 的空闲显存和利用率、一次运行会分到的 GPU,以及远程解释器缺少的训练包。已安装的 torch 没有对应内核的 GPU 会标注为 (torch there has no kernels for it),gpus: auto 会跳过这样的 GPU。--json 以 JSON 打印同样的报告。当一次运行能够启动时,命令以 0 退出,否则以 1 退出。Runs on 菜单旁的 Check 按钮会在训练页面上运行同样的检查。

ssh 运行的步骤

每次运行,Potato 都执行这些步骤:

  1. 用 ssh 连接,并打开一个反向转发,从远程 /tmp 中的 unix 套接字回到标注服务器。
  2. 检查解释器,读取 nvidia-smi,并选定 GPU。
  3. 当主机缺少该版本时,把 Potato 的源码复制到 remote_dir/code/<hash>;当设置了 packages 而环境不存在时,创建虚拟环境。remote_dir 默认为 ~/.cache/potato-training。
  4. 为这次运行启动 python -m potato.training.agent。运行令牌通过 stdin 交给代理,从不出现在命令行上,因为共享服务器上的每个账户都能看到命令行。
  5. 代理下载这次运行的条目、标签和媒体包。代理运行与本地后端相同的训练 worker,并通过套接字把进度、预测和结果送回。
  6. 运行成功后,Potato 把产物目录复制回该次运行的 artifacts/,删除远程作业目录,并关闭连接。

训练页面上的步骤行会跟随这些阶段,例如 copying Potato's code to gpu.example.org 和 waiting for the agent on gpu.example.org。运行日志会写明主机的 Python 和这次运行所分到的 GPU。

完整微调会把整个模型复制回来,在慢速链路上,一个 330 MB 的模型需要几分钟。步骤行会显示模型已到达多少。大于 max_artifact_mb(默认 2048)的文件会留在 GPU 主机上,并在运行日志中列出。fetch_artifacts: false 会把每个模型文件都留在 GPU 主机上,keep_remote: true 则保留远程作业目录以供检查。运行页面上推送到 Hugging Face Hub 的操作需要模型文件在标注服务器上,所以留在 GPU 主机上的模型无法从运行页面推送。

unix 套接字以 0600 权限创建,所以只有 Potato 登录时所用的账户才能通过该套接字连接。通过套接字发来的请求以回环请求的身份到达标注服务器。以 debug: true 启动的服务器会把管理员权限授予回环请求。于是在运行进行期间,任何能使用 GPU 主机上那个账户的人,都对调试服务器拥有管理员权限。通过 ssh 训练的服务器上请保持 debug 关闭。

取消与心跳超时

训练页面上的 Cancel run 通过套接字到达代理,体现在对代理下一次进度报告的响应中。代理向 worker 发送 SIGTERM,如果 worker 在 model_training.grace_seconds(默认 20,由服务器随作业发送)之后仍在运行,则发送 SIGKILL。来自旧版 Potato 服务器的作业不带 grace_seconds,此时代理会等待 20 秒。在服务器上,一旦过了 grace_seconds,这次运行就以已取消结束,服务器随即关闭代理的 ssh 会话。代理在 heartbeat_timeout_s(默认 120)内没有发送任何内容的运行会失败,运行目录中的 agent.log 保存着代理最后的输出。

SLURM 集群

slurm 后端适用于 GPU 位于 SLURM 调度器之后的集群。在这样的集群上,Potato 能通过 ssh 到达的机器是没有 GPU 的登录节点,而计算节点根本无法到达 Potato。因此 Potato 像 ssh 后端那样在登录节点上启动代理,由代理只把训练 worker 用 sbatch 提交到计算节点。进度通过登录节点和计算节点共享的存储上的一个文件传回。

SLURM 后端的要求

登录节点需要一个无提示的 ssh 密钥,以及供代理使用的 Python 3.8 或更新版本,与 ssh 后端的主机相同。登录节点还需要 sbatch、squeue 和 scancel 在 PATH 上。在集群提供 scontrol 命令的地方,代理也会调用它。Potato 从不调用 srun 或 sacct,所以禁用了 srun 且没有计费数据库的集群也能工作。remote_dir 是必需的,并且必须位于计算节点共享的存储上,因为作业要在那里读取数据包并写入模型。位于 /tmp 或 /var/tmp 下的 remote_dir 会被验证器报错,因为这些目录对每个节点都是本地的。

计算节点需要一个带有训练器所需包的解释器,由 job_python 指定。SLURM 后端不支持 packages,因为在登录节点上创建的虚拟环境未必能在计算节点上运行。请把 job_python 指向计算节点上有的环境,并把模块加载或环境激活放在 prelude 中:

yaml
model_training:
  enabled: true
  backends:
    cluster:
      type: slurm
      host: login.cluster.example.org
      python: /opt/conda/bin/python3                    # on the login node, for the agent
      job_python: /shared/envs/train/bin/python         # on the compute nodes, for the worker
      remote_dir: /shared/projects/me/potato-training   # storage the compute nodes share
      partition: gpu
      gres: gpu:1
      time: "04:00:00"
      prelude: ["module load cuda/12.4"]

除 gpus 和 packages 外,每个 ssh 键都适用于 SLURM 后端。GPU 来自 gres,调度器会为它所分配的 GPU 设置 CUDA_VISIBLE_DEVICES。没有 gres 的作业得不到 GPU。SLURM 设置对应到 sbatch 的标志:

键默认值含义
job_python后端的 pythonworker 在计算节点上的解释器。
gres无sbatch --gres,例如 gpu:1 或 gpu:A6000:1。
time04:00:00sbatch --time。超过 6 小时时,potato validate 和服务器启动会报告一条警告,详见下一段。
partition集群的默认值sbatch --partition。
nice0sbatch --nice,取值 0 到 2000。
cpus_per_task, mem集群的默认值sbatch --cpus-per-task 和 --mem。
sbatch_options[]更多标志,例如 ["--requeue", "--constraint=a100"]。Potato 自己设置 --output、--chdir、--job-name 和 --parsable,验证器会对这里出现的这些标志报错。
prelude[]在作业中、worker 之前运行的 shell 行。
poll_interval_s30两次 squeue 调用之间的秒数,至少为 10。
exit_grace_s180等待已结束作业的退出码出现在登录节点上的秒数,至少为 20。
job_namepotato-train作业名的前缀,后面是运行 id 的前 8 个字符。

time 超过 6 小时时,potato validate 和服务器启动会报告 model_training.backends.cluster.time is 12:00:00. A job that holds a GPU for more than 6 hours is long on a shared cluster; check its policy,而 potato validate --strict 会因这条警告而失败。一旦集群的政策允许更长的作业,就在顶层的 validation 块中为这一个后端确认该警告,该块只有 potato validate 会读取:

yaml
validation:
  acknowledge:
    - "model_training.backends.cluster.time is"

每个条目都按区分大小写、至少 12 个字符的子串与警告文本匹配,匹配上的警告会移入已确认列表,不再使 --strict 失败。写明后端名称的条目只为该后端确认警告。过短或没有匹配任何警告的条目本身会成为一条警告。

SLURM 后端的检查命令

bash
potato training check cluster config.yaml

该检查会登录到登录节点,确认 sbatch、squeue 和 scancel 在 PATH 上,并确认 remote_dir 不在 /tmp 下。报告会写明登录节点上的 Python 版本,以及作业将运行的 job_python。检查无法看到计算节点有哪些包,所以计算节点上缺少的包会表现为运行失败,导入错误记在运行日志中。请用较小的 time 先跑一个简短的作业,尽早发现这类问题。

SLURM 运行的步骤

Potato 连接到登录节点并把代码复制过去,与 ssh 后端相同。随后代理执行这些步骤:

  1. 把数据包下载到 remote_dir/jobs/<run>/。
  2. 写出 job.sh,并用 sbatch --parsable 提交该脚本。运行日志会记录 Submitted SLURM job,附上作业 id 和各标志。
  3. 在作业目录中,计算节点上的 worker 把事件写入 events.jsonl,把错误写入 stderr.log,把退出码写入 exit_code。SLURM 自己的输出写入 slurm.out。
  4. 代理随文件增长读取 events.jsonl,并把进度转发给服务器。代理每隔 poll_interval_s 运行一次 squeue,运行日志会记录作业队列状态的每次变化,例如 SLURM job 4242: PENDING (Priority),然后是 RUNNING on node07。
  5. 作业结束时,代理上传预测并报告结果。产物回到服务器,作业目录被删除,与 ssh 后端相同。

Cancel run 会向作业发送 scancel --full --signal=TERM,如果 worker 尚未停止,再发一个普通的 scancel。当到登录节点的 ssh 会话断开时,代理会取消自己的作业,GPU 分配随之释放。当调度器自己结束一个作业时,这次运行会以调度器给出的原因失败,原因读自 scontrol show job 或 slurm.out。Potato 报告的原因包括超出时间限制、节点故障、被抢占、内存耗尽,以及来自 Potato 之外的 scancel。

Potato 的测试套件对 sbatch、squeue 和 scancel 的替身运行 SLURM 流程,覆盖作业提交、队列状态、进度转发和取消。分区限制和抢占政策因集群而异,所以在新集群上请留意第一个作业的运行日志。

在一个能到达服务器的登录节点上手工启动的代理,同样可以向 SLURM 提交作业,命令是 potato training agent --server ... --backend <name> --workdir <shared dir> --slurm-config slurm.json。文件 slurm.json 以 JSON 对象的形式保存上表中的设置,--workdir 必须位于计算节点共享的存储上。

Potato 无法到达的机器

agent 后端适用于标注服务器无法连接的机器,例如 NAT 后的工作站。有人在那台机器上启动 potato training agent,代理便向服务器轮询作业。代理不需要任何入站端口,因为所有连接都由代理自己建立。取消请求会在服务器对代理下一次进度报告的响应中到达代理。模型文件留在训练机器上,只有预测和结果会传回。

agent 后端的要求

训练机器需要安装 Potato、你所运行训练器的包,并且要有到标注服务器 URL 的网络路径。对 hf-text-classification 而言,一条安装命令就涵盖了 Potato 和训练器的包:

bash
pip install "potato-annotation[train-text]"

服务器只需要后端条目。agent 后端接受 type、description、heartbeat_timeout_s(默认 120)和 queue_timeout_s,不接受其他键:

yaml
model_training:
  enabled: true
  backends:
    lab-workstation:
      type: agent
      queue_timeout_s: 3600

注册并启动代理

在标注服务器上为该后端签发一个令牌:

bash
potato training register lab-workstation config.yaml

该命令只打印一次令牌,并附上要在训练机器上运行的命令:

text
Token for backend 'lab-workstation' (shown once; registering again replaces it):
 
  <token>
 
On the training machine:
 
  export POTATO_TRAINING_TOKEN=<token>
  potato training agent --server http://<this-server>:8000 --backend lab-workstation

在训练机器上用服务器的真实 URL 启动代理:

bash
export POTATO_TRAINING_TOKEN=<the token>
potato training agent --server https://annotate.example.org --backend lab-workstation

代理会为它的后端运行作业,直到你停止代理。--token-file 从文件而不是 POTATO_TRAINING_TOKEN 读取令牌,--python 指定训练 worker 运行所用的解释器,默认是代理自己的解释器。--once 在一个作业之后退出,--workdir 把代理的文件从 ~/.cache/potato-training 移到别处,--keep 则保留每个作业的数据包。再次注册同一名称会签发新令牌,并使旧令牌失效。

potato training check lab-workstation config.yaml 会打印该后端的令牌记录,一旦有代理连接过,last_seen 就会被设置。注册之前,该命令会打印 no token issued yet 以及要运行的注册命令。

agent 运行的步骤

针对 agent 后端的运行会停留在 queued 状态,直到有代理来领取工作。设置了 queue_timeout_s: 3600 时,没有代理的运行会在一小时后失败,没有 queue_timeout_s 时,运行会一直等到你取消它。代理以 tar.gz 文件的形式下载数据包,并从中断处恢复被中断的下载,最多尝试五次。随后代理运行 worker,发布进度,上传 predictions.jsonl,并报告退出码。对于大于 max_predictions_mb(默认 512)的预测文件,服务器回应 413。

模型文件留在训练机器的 --workdir 下,运行记录的 artifact_uri 写明主机和目录。如果运行期间代理有 300 秒无法连接服务器,代理会自行取消该次运行。代理在 heartbeat_timeout_s 内没有发送任何内容的运行,会在服务器上以 No word from the agent on lab-workstation for 120 seconds 失败。

常见故障

每一项先给出运行日志或检查报告中的消息,再给出修复办法。适用于每种后端和训练器的完整列表在上游的故障排查一节。

  • ssh to gpu.example.org failed: Permission denied。 Potato 需要一个无提示即可使用的密钥。从服务器的账户运行 ssh gpu.example.org true 必须成功。
  • /opt/conda/bin/python3 was not found on gpu.example.org。 把后端中的 python 设为主机上存在的解释器。
  • No GPU has 4000 MB free。 主机上的每块 GPU 都比 min_free_gpu_mb 允许的更忙。potato training check gpu-server config.yaml 会列出这些 GPU。可以等待、降低阈值,或用 gpus: "1" 固定一块 GPU。
  • Trainer 'hf-text-classification' could not be imported。 远程解释器缺少训练器需要的某个包。把 python 指向有该包的解释器,或把该包列在 packages 下。在 SLURM 后端上,请修复 job_python 所指的环境。
  • This host's torch has no kernels for any of its GPUs。 远程 torch 是为比主机所拥有的更新的 GPU 构建的。请安装适配这些 GPU 的 torch 构建,或改用另一台主机。
  • No word from the agent on gpu-server for 120 seconds。 远程机器或网络消失了。在 ssh 后端上,运行目录中的 agent.log 有代理最后的输出。
  • The copy from gpu.example.org sent nothing for 180 s。 模型被复制回来的过程中,到主机的网络中断了。这次运行的预测已经保存,artifacts/ 中只缺少模型文件。
  • login.cluster.example.org has no sbatch on PATH, so it cannot submit jobs。 host 不是集群的登录节点,或者这些命令所在的 PATH 只有登录 shell 才会设置。请在后端的 env 下设置 PATH。
  • SLURM job 4242 ended before the worker finished: it reached its time limit。 提高后端中的 time,或用更少的条目训练。设置 keep_remote: true 可保留 slurm.out,以便在其他调度器故障之后阅读。
  • agent 后端上的运行一直处于排队状态。 没有使用该后端名称的代理在轮询。请用 potato training agent --backend lab-workstation 启动代理,并通过训练页面上的 Check 检查令牌。
  • nvrtc: error: failed to open libnvrtc-builtins.so.13.0。 主机的 torch 与它的 nvidia-cuda-nvrtc 包对应不同的 CUDA 版本。请安装与 torch 构建匹配的 nvidia-cuda-nvrtc,或把 python 指向另一个解释器。

延伸阅读

  • 模型训练概览介绍训练页面、训练循环,以及把模型发布到 Hugging Face Hub。
  • 配置列出每个 model_training 键、留出得分,以及运行的存放位置。
  • 微调 LLM介绍最常需要远程 GPU 的那些训练器,涉及 LoRA 和 4-bit 基础权重。
  • 上游关于运行在哪里执行的参考给出了每种后端的完整键表,代理协议一节则为自行编写代理的人记录了各路由。