Ray Tune 常见问题#

这里我们尝试回答一些常见问题。如果您在阅读此常见问题解答后仍有问题,请告诉我们!

什么是超参数?#

什么是超参数?它们与模型参数有何不同?

在监督学习中,我们使用标记数据训练模型,以便模型能够正确识别新的数据值。模型的方方面面都由一组参数定义,例如线性回归中的权重。这些是模型参数;它们在训练过程中学习得到。

../_images/hyper-model-parameters.png

相比之下,超参数定义了模型本身的结构细节,例如我们是使用线性回归还是分类,神经网络的最佳架构是什么,有多少层,使用哪种滤波器等。它们在训练之前定义,而不是学习得到。

../_images/hyper-network-params.png

其他被认为是超参数的量包括学习率、折扣率等。如果想让我们的训练过程和得到的模型表现良好,首先需要确定最优或接近最优的超参数集。

如何确定最优超参数?最直接的方法是执行一个循环,从一些合理包含的可能值列表中选取一组候选值,训练模型,将获得的结果与之前的循环迭代进行比较,然后选取表现最好的一组。这个过程称为超参数调优优化 (HPO)。超参数在配置好的限定搜索空间内指定,为 config 字典中的每个超参数共同定义。

我应该选择哪种搜索算法/调度器?#

Ray Tune 提供了许多不同的搜索算法调度器。选择哪种主要取决于您的问题

  • 这是一个小问题还是大问题(训练需要多长时间?资源成本如何,比如 GPU)?您能否并行运行许多试验?

  • 您想调优多少个超参数?

  • 超参数的有效值是什么?

如果您的模型返回增量结果(例如,深度学习中每轮 epoch 的结果,GBDT 中每添加一棵树的结果等),使用早期停止通常可以采样更多配置,因为没有前途的试验会在运行完整个过程之前被剪枝。请注意,并非所有搜索算法都能利用剪枝试验的信息。如果没有增量结果,则无法使用早期停止 - 在函数式 API 的情况下,这意味着 session.report() 必须被多次调用 - 通常在循环中。

如果您的模型较小,通常可以尝试运行许多不同的配置。可以使用随机搜索生成配置。也可以对某些值进行网格搜索。您可能仍然应该使用ASHA 来早期终止表现不佳的试验(如果您的问题支持早期停止)。

如果您的模型较大,可以尝试使用基于贝叶斯优化的搜索算法,如BayesOpt,在少量试验后获得良好的参数配置。Ax 类似,但对噪声数据更鲁棒。请注意,这些算法仅在超参数数量较少时效果良好。或者,您可以使用基于群体训练,它在少量试验(例如 8 个甚至 4 个)时效果良好。然而,这会输出一个超参数调度,而不是一个固定的超参数集。

如果您的超参数数量较少,贝叶斯优化方法效果良好。看看BOHB 或结合ASHA 调度器Optuna,以结合贝叶斯优化和早期停止的优点。

如果您的超参数只有连续值,这与大多数贝叶斯优化方法都能很好地配合。离散或分类变量仍然可以使用,但类别数量越多,效果越差。

如果您的超参数有许多分类值,请考虑使用随机搜索,或者基于 TPE 的贝叶斯优化算法,例如OptunaHyperOpt

我们的首选解决方案通常是对于较小的问题使用随机搜索ASHA 进行早期停止。对于超参数数量较少的大问题使用BOHB,如果学习调度可以接受,则对于超参数数量较多的大问题使用基于群体训练

如何选择超参数范围?#

一个好的开始是查看引入这些算法的论文,也看看其他人正在使用什么。

大多数算法对其部分参数也有合理的默认值。例如,XGBoost 的参数概述报告决策树的最大深度使用 max_depth=6。在这里,2 到 10 之间的任何值可能都有意义(尽管这自然取决于您的问题)。

对于学习率,我们建议使用介于 1e-51e-1 之间的对数均匀分布tune.loguniform(1e-5, 1e-1)

对于批量大小,我们建议尝试 2 的幂次方,例如 2、4、8、16、32、64、128、256 等。具体大小取决于您的问题。对于数据量大且容易的问题,使用更大的批量大小;对于数据量不多且困难的问题,使用更小的批量大小。

对于层大小,我们也建议尝试 2 的幂次方。对于小问题(例如 Cartpole),使用较小的层大小。对于大问题,尝试较大的层大小。

对于强化学习中的折扣因子,我们建议在 0.9 到 1.0 之间均匀采样。根据问题不同,可能更有意义的是使用高于 0.97 甚至高于 0.99 的更严格范围(例如 Atari)。

如何使用嵌套/条件搜索空间?#

有时您可能需要定义其值依赖于其他参数值的参数。Ray Tune 提供了一些方法来定义这些。

嵌套空间#

您可以在子字典中嵌套超参数定义

config = {"a": {"x": tune.uniform(0, 10)}, "b": tune.choice([1, 2, 3])}

试验配置将与输入配置完全一样地嵌套。

条件空间#

这里详细解释了自定义和条件搜索空间。简而言之,您可以将自定义函数传递给 tune.sample_from(),这些函数可以返回依赖于其他值的值。

config = {
    "a": tune.randint(5, 10),
    "b": tune.sample_from(lambda spec: np.random.randint(0, spec.config.a)),
}

早期终止(例如 Hyperband/ASHA)如何工作?#

早期终止算法会查看中间报告的值,例如在每个训练 epoch 后通过 session.report() 报告给它们的值。在一定步数后,它们会移除表现最差的试验,只保留表现最好的试验。试验的好坏由按目标指标(例如准确率或损失)对其排序来确定。

在 ASHA 中,您可以决定提前终止多少试验。reduction_factor=4 意味着每次缩减时只保留 25% 的试验。使用 grace_period=n,您可以强制 ASHA 至少训练每个试验 n 个 epoch。

为什么我的所有试验都返回“1”次迭代?#

这很可能适用于 Tune 函数 API。

Ray Tune 在 session.report() 每次被调用时会在内部计数迭代次数。如果您只在训练结束时调用一次 session.report(),计数器就只增加了一次。如果您使用类 API,计数器在调用 step() 后增加。

请注意,多次报告指标可能更有意义。例如,如果您训练算法 1000 个时间步,考虑每隔 100 步报告一次中间性能值。这样,Hyperband/ASHA 等调度器可以提前终止表现不佳的试验。

这些额外的输出是什么?#

您会注意到 Ray Tune 不仅报告超参数(来自 config)或指标(传递给 session.report()),还报告一些其他输出。

Result for easy_objective_c64c9112:
  date: 2020-10-07_13-29-18
  done: false
  experiment_id: 6edc31257b564bf8985afeec1df618ee
  experiment_tag: 7_activation=tanh,height=-53.116,steps=100,width=13.885
  hostname: ubuntu
  iterations: 0
  iterations_since_restore: 1
  mean_loss: 4.688385317424468
  neg_mean_loss: -4.688385317424468
  node_ip: 192.168.1.115
  pid: 5973
  time_since_restore: 7.605552673339844e-05
  time_this_iter_s: 7.605552673339844e-05
  time_total_s: 7.605552673339844e-05
  timestamp: 1602102558
  timesteps_since_restore: 0
  training_iteration: 1
  trial_id: c64c9112

有关术语表,请参阅如何在 Tune 中使用日志指标? 部分。

如何设置资源?#

如果您想为试验分配特定资源,可以使用 tune.with_resources 并将其与 dict 或 PlacementGroupFactory 对象一起包裹在您的 trainable 周围。

tuner = tune.Tuner(
    tune.with_resources(
        train_fn, resources={"cpu": 2, "gpu": 0.5, "custom_resources": {"hdd": 80}}
    ),
)
tuner.fit()

上面的示例展示了三点

  1. cpugpu 选项分别设置每个试验可用的 CPU 和 GPU 数量。试验不能请求超过这些资源的量(例外:参见第 3 点)。

  2. 可以请求小数 GPU。值为 0.5 意味着 GPU 一半的内存可用于试验。您需要自行确保您的模型仍然可以在这部分内存中运行。

  3. 您可以请求启动集群时提供给 Ray 的自定义资源。试验只会被调度到可以提供您请求的所有资源的单个节点上。

需要记住的一件重要事情是,每个 Ray worker(以及每个 Ray Tune Trial)只会调度到一台机器上。这意味着如果您例如为试验请求 2 个 GPU,但您的集群由 4 台机器组成,每台机器有 1 个 GPU,那么该试验永远不会被调度。

换句话说,您必须确保您的 Ray 集群拥有能够实际满足您资源请求的机器。

在某些情况下,您的 trainable 可能希望启动其他远程 actor,例如如果您通过 Ray Train 利用分布式训练。在这种情况下,您可以使用placement groups 请求额外资源

tuner = tune.Tuner(
    tune.with_resources(
        train_fn,
        resources=tune.PlacementGroupFactory(
            [
                {"CPU": 2, "GPU": 0.5, "hdd": 80},
                {"CPU": 1},
                {"CPU": 1},
            ],
            strategy="PACK",
        ),
    )
)
tuner.fit()

在这里,您为远程任务请求了 2 个额外的 CPU。这两个额外的 actor 不一定必须与您的主 trainable 位于同一节点上。实际上,您可以通过 strategy 参数控制这一点。在此示例中,PACK 将尝试将 actor 调度到同一节点上,但也允许它们调度到其他节点上。请参阅placement groups 文档以了解有关这些放置策略的更多信息。

您还可以通过 lambda 函数根据自定义规则为试验分配特定资源。例如,如果您想根据参数空间中的设置向试验分配 GPU 资源

tuner = tune.Tuner(
    tune.with_resources(
        train_fn,
        resources=lambda config: {"GPU": 1} if config["use_gpu"] else {"GPU": 0},
    ),
    param_space={
        "use_gpu": True,
    },
)
tuner.fit()

为什么我的训练卡住了,Ray 报告说挂起的 actor 或任务无法调度?#

这通常是由于 trainable 启动了 Ray actor 或任务,但 trainable 资源没有考虑它们,从而导致死锁。在 trainable 中使用基于 Ray 的其他库(如 Modin)也可能“偷偷地”导致此问题。为了解决此问题,请按照上面章节的说明,使用placement groups 为试验请求额外资源。

例如,如果您的 trainable 使用 Modin 数据框,对这些数据框的操作将生成 Ray 任务。通过为试验分配额外的 CPU 捆绑,这些任务将能够在不缺乏资源的情况下运行。

def train_fn(config):
    # some Modin operations here
    # import modin.pandas as pd
    tune.report({"metric": metric})

tuner = tune.Tuner(
    tune.with_resources(
        train_fn,
        resources=tune.PlacementGroupFactory(
            [
                {"CPU": 1},  # this bundle will be used by the trainable itself
                {"CPU": 1},  # this bundle will be used by Modin
            ],
            strategy="PACK",
        ),
    )
)
tuner.fit()

如何向我的 trainable 传递更多参数值?#

Ray Tune 要求您的 trainable 函数最多接受两个参数:configcheckpoint_dir。但有时您希望传递常量参数,例如要运行的 epoch 数或用于训练的数据集。Ray Tune 提供了一个 wrapper 函数来实现这一点,称为tune.with_parameters()

from ray import tune
import numpy as np


def train_func(config, num_epochs=5, data=None):
    for i in range(num_epochs):
        for sample in data:
            # ... train on sample
            pass


# Some huge dataset
data = np.random.random(size=100000000)

tuner = tune.Tuner(tune.with_parameters(train_func, num_epochs=5, data=data))
tuner.fit()

此函数类似于 functools.partial,但它直接将参数存储在 Ray 对象存储中。这意味着即使是数据集这样的大对象,您也可以传递,并且 Ray 会确保这些对象在您的集群机器上高效地存储和检索。

tune.with_parameters() 也适用于类 trainables。有关更多详细信息和示例,请参阅tune.with_parameters()

如何复现实验?#

机器学习运行的精确复现很难实现。在分布式环境中更是如此,因为会引入更多非确定性。例如,如果两个试验同时完成,搜索算法的收敛可能会受到哪个试验结果首先处理的影响。这取决于搜索器——对于随机搜索,这应该没有区别,但对于大多数其他搜索器则会有影响。

随机数生成器用于创建随机性,例如为您定义的参数采样超参数值。计算中没有真正的随机性,而是存在一些复杂的算法,它们生成的数字看似随机并满足随机分布的所有属性。这些算法可以用一个初始状态进行种子设定,之后生成的随机数总是相同的。

import random

random.seed(1234)
output = [random.randint(0, 100) for _ in range(10)]

# The output will always be the same.
assert output == [99, 56, 14, 0, 11, 74, 4, 85, 88, 10]

Python 库中最常用的随机数生成器是原生 random 子模块和 numpy.random 模块中的生成器。

# This should suffice to initialize the RNGs for most Python-based libraries
import random
import numpy as np

random.seed(1234)
np.random.seed(5678)

在您的调优和训练运行中,随机性出现在多个地方,在所有这些地方,我们都需要引入种子以确保获得相同的行为。

  • 搜索算法:搜索算法必须通过种子来确保每次运行生成相同的超参数配置。某些搜索算法可以通过随机种子显式实例化(在构造函数中查找 seed 参数)。对于其他算法,尝试使用上面的代码块。

  • 调度器:Population Based Training 等调度器依赖于重新采样一些参数,需要随机性。使用上面的代码块设置初始种子。

  • 训练函数:除了初始化配置外,训练函数本身也必须使用种子。这可能涉及例如数据分割。您应该确保在训练函数开始时设置种子。

PyTorch 和 TensorFlow 使用它们自己的 RNG,也必须进行初始化

import torch

torch.manual_seed(0)

import tensorflow as tf

tf.random.set_seed(0)

因此,您应该为 Ray Tune 的调度器和搜索算法以及训练代码设置种子。调度器和搜索算法应该始终使用相同的种子进行种子设定。训练代码也是如此,但通常最好让不同训练运行之间的种子不同。

这是在您的训练代码中完成所有这些操作的蓝图

import random
import numpy as np
from ray import tune


def trainable(config):
    # config["seed"] is set deterministically, but differs between training runs
    random.seed(config["seed"])
    np.random.seed(config["seed"])
    # torch.manual_seed(config["seed"])
    # ... training code


config = {
    "seed": tune.randint(0, 10000),
    # ...
}

if __name__ == "__main__":
    # Set seed for the search algorithms/schedulers
    random.seed(1234)
    np.random.seed(1234)
    # Don't forget to check if the search alg has a `seed` parameter
    tuner = tune.Tuner(trainable, param_space=config)
    tuner.fit()

请注意,并非总是能够控制所有非确定性来源。例如,如果您使用 ASHA 或 PBT 等调度器,某些试验可能比其他试验更早完成,从而影响调度器的行为。然而,哪些试验首先完成可能取决于当前系统负载、网络通信或环境中我们无法通过随机种子控制的其他因素。对于贝叶斯优化等搜索算法也是如此,它们在采样新配置时会考虑之前的结果。这可以通过使用 PBT 和 Hyperband 的同步模式来解决,在这些模式下,调度器会等待所有试验完成一个 epoch 后再决定提升哪些试验。

我们强烈建议先在较小的玩具问题上尝试复现,然后再将其用于较大的实验。

如何避免瓶颈?#

有时您可能会遇到类似这样的消息

The `experiment_checkpoint` operation took 2.43 seconds to complete, which may be a performance bottleneck

最常见的情况是 experiment_checkpoint 操作会抛出此警告,但也可能是其他操作,例如 process_trial_result

这些操作通常应在 500 毫秒内完成。如果持续时间更长,可能表明存在问题或效率低下。要消除此消息,了解其来源非常重要。

出现此问题的主要原因如下

Trial config 非常大

如果您尝试通过 config 参数传递数据集或其他大对象,就会出现这种情况。在这种情况下,数据集在实验检查点期间会反复序列化并写入磁盘,这需要很长时间。

解决方案:使用tune.with_parameters 通过对象存储将大对象传递给函数 trainables。对于类 trainables,您可以通过 ray.put()ray.get() 手动完成此操作。如果您需要传递类定义,请考虑传递一个指示符(例如字符串),然后让 trainable 选择该类。通常,您的 config 字典应该只包含基本类型,如数字或字符串。

Trial 结果非常大

如果您通过类 trainable 的 step() 返回值或函数 trainable 的 session.report() 返回对象、数据或其他大对象,就会出现这种情况。效果与上述相同:结果会反复序列化并写入磁盘,这会花费很长时间。

解决方案:使用检查点,将数据写入 trainable 的当前工作目录。根据您使用的是类 API 还是函数 API,有多种方法可以实现。

您在集群上训练大量试验,或者您正在保存巨大的检查点

解决方案:您可以使用云检查点将日志和检查点保存到指定的 storage_path。这是处理此问题的首选方法。所有同步将自动处理,因为所有节点都可以访问云存储。此外,您的结果将是安全的,即使您在使用可抢占实例,也不会丢失任何数据。

您报告结果过于频繁

每个结果都由搜索算法、试验调度器和回调(包括日志记录器和试验同步器)处理。如果您每个试验报告大量结果(例如每秒多个结果),这会花费很长时间。

解决方案:这里的解决方案很明显:不要那么频繁地报告结果。在类 trainables 中,step() 可以处理更大块的数据。在函数 trainables 中,您可以在训练循环的每 n 次迭代时报告一次。尝试权衡您真正需要的结果数量,以便进行调度或搜索决策。如果您需要更精细的指标进行日志记录或跟踪,请考虑为此使用单独的日志记录机制,而不是 Ray Tune 提供的结果进度日志记录。

如何在本地开发和测试 Tune?#

首先,按照构建 Ray (仅 Python) 中的说明开发 Tune,无需编译 Ray。设置好 Ray 后,运行 pip install -r ray/python/ray/tune/requirements-dev.txt 安装 Tune 开发所需的所有包。现在,要运行所有 Tune 测试,只需运行

pytest ray/python/ray/tune/tests/

如果您计划提交拉取请求,建议您事先在本地运行单元测试,以加快审查过程。尽管我们有 hooks 为每个拉取请求自动运行单元测试,但通常在您的机器上先运行它们会更快,以避免任何明显的错误。

如何开始为 Tune 做贡献?#

我们使用 Github 来跟踪问题、功能请求和 bug。查看标记为“good first issue”“help wanted” 的问题,作为开始的地方。查找标题中包含 “[tune]” 的问题。

注意

如果提出与 Tune 相关的新问题或 PR,请务必在标题中包含 “[tune]” 并添加 tune 标签。

如何使我的 Tune 实验可复现?#

机器学习运行的精确复现很难实现。在分布式环境中更是如此,因为会引入更多非确定性。例如,如果两个试验同时完成,搜索算法的收敛可能会受到哪个试验结果首先处理的影响。这取决于搜索器——对于随机搜索,这应该没有区别,但对于大多数其他搜索器则会有影响。

如果您尝试达到一定程度的可复现性,则需要在两个地方设置随机种子

  1. 在驱动程序上,例如用于搜索算法。这将确保至少搜索算法建议的初始配置是相同的。

  2. 在 trainable 中(如果需要)。神经网络通常用随机数初始化,许多经典 ML 算法(如 GBDTs)也利用随机性。因此,您需要确保在此处设置种子,以便初始化始终相同。

这是一个始终产生相同结果(试验运行时长除外)的示例。

import numpy as np
from ray import tune


def train_func(config):
    # Set seed for trainable random result.
    # If you remove this line, you will get different results
    # each time you run the trial, even if the configuration
    # is the same.
    np.random.seed(config["seed"])
    random_result = np.random.uniform(0, 100, size=1).item()
    tune.report({"result": random_result})


# Set seed for Ray Tune's random search.
# If you remove this line, you will get different configurations
# each time you run the script.
np.random.seed(1234)
tuner = tune.Tuner(
    train_func,
    tune_config=tune.TuneConfig(
        num_samples=10,
        search_alg=tune.search.BasicVariantGenerator(),
    ),
    param_space={"seed": tune.randint(0, 1000)},
)
tuner.fit()

一些搜索器使用自己的随机状态来采样新配置。这些搜索器通常接受一个 seed 参数,可以在初始化时传递。其他搜索器使用 Numpy 的 np.random 接口 - 这些种子可以通过 np.random.seed() 来设置。我们在搜索器类中不提供执行此操作的接口,因为全局设置随机种子可能会产生副作用。例如,它可能会影响数据集的分割方式。因此,我们将其留给用户来完成这些全局配置更改。

如何在 Tune 中使用大型数据集?#

您通常希望在驱动程序上计算一个大对象(例如,训练数据、模型权重),并在每个试验中使用该对象。

Tune 提供了一个包装函数 tune.with_parameters(),允许您将大对象广播到您的 trainable。使用此包装函数传递的对象将存储在Ray 对象存储中,并将自动获取并作为参数传递给您的 trainable。

提示

如果对象大小较小或已存在于Ray 对象存储中,则无需使用 tune.with_parameters()。您可以改用partials 或直接传递给 config

from ray import tune
import numpy as np


def f(config, data=None):
    pass
    # use data


data = np.random.random(size=100000000)

tuner = tune.Tuner(tune.with_parameters(f, data=data))
tuner.fit()

如何将我的 Tune 结果上传到云存储?#

请参阅使用云存储配置 Tune(AWS S3、Google Cloud Storage)

确保 worker 节点具有云存储的写入权限。否则会导致类似 Error message (1): fatal error: Unable to locate credentials 的错误消息。对于 AWS 设置,这涉及为 worker 节点添加 IamInstanceProfile 配置。请参阅此处获取更多技巧。

如何在 Tune 中使用 Kubernetes?#

您应该配置共享存储。请参阅此用户指南:如何在 Ray Tune 中配置持久化存储

如何在 Tune 中使用 Docker?#

您应该配置共享存储。请参阅此用户指南:如何在 Ray Tune 中配置持久化存储

如何配置搜索空间?#

您可以通过传递给 Tuner(param_space=...) 的 dict 指定网格搜索或采样分布。

parameters = {
    "qux": tune.sample_from(lambda spec: 2 + 2),
    "bar": tune.grid_search([True, False]),
    "foo": tune.grid_search([1, 2, 3]),
    "baz": "asd",  # a constant value
}

tuner = tune.Tuner(train_fn, param_space=parameters)
tuner.fit()

默认情况下,每个随机变量和网格搜索点采样一次。要进行多次随机采样,请将 num_samples: N 添加到实验 config 中。如果提供了 grid_search 参数,则网格将重复 num_samples 次。

# num_samples=10 repeats the 3x3 grid search 10 times, for a total of 90 trials
tuner = tune.Tuner(
    train_fn,
    run_config=tune.RunConfig(name="my_trainable"),
    param_space={
        "alpha": tune.uniform(100, 200),
        "beta": tune.sample_from(lambda spec: spec.config.alpha * np.random.normal()),
        "nn_layers": [
            tune.grid_search([16, 64, 256]),
            tune.grid_search([16, 64, 256]),
        ],
    },
    tune_config=tune.TuneConfig(num_samples=10),
)

请注意,搜索空间可能无法跨不同搜索算法互操作。例如,对于许多搜索算法,您将无法使用 grid_searchsample_from 参数。请在搜索空间 API 页面中阅读相关内容。

如何在我的 Tune 训练函数中访问相对文件路径?#

假设您在 ~/code 内部使用 my_script.py 启动一个 Tune 实验。默认情况下,Tune 会将每个 worker 的工作目录更改为其对应的试验目录(例如 ~/ray_results/exp_name/trial_0000x)。这保证了每个 worker 进程都有单独的工作目录,避免在保存试验特定输出时发生冲突。

您可以通过设置 RAY_CHDIR_TO_TRIAL_DIR=0 环境变量来配置此项。这明确告诉 Tune 不要将工作目录更改为试验目录,从而可以访问相对于原始工作目录的路径。一个注意事项是,工作目录现在在 worker 之间共享,因此应使用tune.get_context().get_trial_dir() API 获取保存试验特定输出的路径。

def train_func(config):
    # Read from relative paths
    print(open("./read.txt").read())

    # The working directory shouldn't have changed from the original
    # NOTE: The `TUNE_ORIG_WORKING_DIR` environment variable is deprecated.
    assert os.getcwd() == os.environ["TUNE_ORIG_WORKING_DIR"]

    # Write to the Tune trial directory, not the shared working dir
    tune_trial_dir = Path(ray.tune.get_context().get_trial_dir())
    with open(tune_trial_dir / "write.txt", "w") as f:
        f.write("trial saved artifact")

os.environ["RAY_CHDIR_TO_TRIAL_DIR"] = "0"
tuner = tune.Tuner(train_func)
tuner.fit()

警告

TUNE_ORIG_WORKING_DIR 环境变量是访问相对于原始工作目录的路径的原始解决方法。此环境变量已弃用,应改为使用上面的 RAY_CHDIR_TO_TRIAL_DIR 环境变量。

如何同时在同一集群上运行多个 Ray Tune 作业(多租户)?#

Ray Tune 不官方支持在同一集群上同时运行多个 Ray Tune 运行。我们不对该工作流程进行测试,建议为每个调优作业使用单独的集群。

原因如下

  1. 当多个 Ray Tune 作业同时运行时,它们会竞争资源。一个作业可能会同时运行其所有试验,而另一个作业则需要长时间等待才能获得资源来运行第一个试验。

  2. 如果您的基础设施上很容易启动新的 Ray 集群,那么运行一个大型集群通常没有比运行多个小型集群带来成本优势。例如,运行一个包含 32 个实例的集群与运行 4 个每个包含 8 个实例的集群成本几乎相同。

  3. 并发作业更难调试。如果作业 A 的一个试验占满了磁盘空间,同一节点上作业 B 的试验也会受到影响。实际上,如果出现问题,很难从日志中推断出这些情况。

以前,Ray Tune 中的一些内部实现假定您一次只运行一个作业。一个症状是作业 A 的试验使用了作业 B 中指定的参数,导致意外结果。

如果您遇到此问题,请参阅 [this github issue](ray-project/ray#30091) 以获取更多背景信息和解决方法。

如何继续训练已完成的 Tune 实验,使其运行更长时间并使用新配置(迭代实验)?#

假设我有一个 Tune 实验已完成,配置如下

import os
import tempfile

import torch

from ray import tune
from ray.tune import Checkpoint
import random


def trainable(config):
    for epoch in range(1, config["num_epochs"]):
        # Do some training...

        with tempfile.TemporaryDirectory() as tempdir:
            torch.save(
                {"model_state_dict": {"x": 1}}, os.path.join(tempdir, "model.pt")
            )
            tune.report(
                {"score": random.random()},
                checkpoint=Checkpoint.from_directory(tempdir),
            )


tuner = tune.Tuner(
    trainable,
    param_space={"num_epochs": 10, "hyperparam": tune.grid_search([1, 2, 3])},
    tune_config=tune.TuneConfig(metric="score", mode="max"),
)
result_grid = tuner.fit()

best_result = result_grid.get_best_result()
best_checkpoint = best_result.checkpoint

现在,我想从上一个实验生成的检查点(例如最好的一个)继续训练,并在新的超参数搜索空间中进行搜索,再训练 10 个 epoch。

如何在 Ray Tune 中启用容错 解释了 Tuner.restore 的用法旨在根据初始训练运行中提供的确切配置,恢复在中间中断的未完成实验。

因此,Tuner.restore 不适用于我们期望的行为。这种“迭代实验”的方式应该使用新的 Tune 实验来完成,而不是反复恢复单个实验并修改实验规范。

请参见以下示例,了解如何创建基于旧实验的新实验

import ray


def trainable(config):
    # Add logic to handle the initial checkpoint.
    checkpoint: Checkpoint = config["start_from_checkpoint"]
    with checkpoint.as_directory() as checkpoint_dir:
        model_state_dict = torch.load(os.path.join(checkpoint_dir, "model.pt"))

    # Initialize a model from the checkpoint...
    # model = ...
    # model.load_state_dict(model_state_dict)

    for epoch in range(1, config["num_epochs"]):
        # Do some more training...
        ...

        tune.report({"score": random.random()})


new_tuner = tune.Tuner(
    trainable,
    param_space={
        "num_epochs": 10,
        "hyperparam": tune.grid_search([4, 5, 6]),
        "start_from_checkpoint": best_checkpoint,
    },
    tune_config=tune.TuneConfig(metric="score", mode="max"),
)
result_grid = new_tuner.fit()