Ray Tune FAQ#

Here we try to answer questions that come up often. If you still have questions after reading this FAQ, let us know!

什么是超参数?#

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

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

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

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

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

其他被视为超参数的数量包括学习率、折扣率等。如果我们希望我们的训练过程和最终模型能够良好工作,我们首先需要确定一组最优或接近最优的超参数

我们如何确定最优超参数?最直接的方法是执行一个循环,从中选择一组候选值,从一组合理且包容的可能性值列表中选择,训练模型,比较与前一个循环迭代达到的结果,然后选择表现最佳的集合。这个过程称为超参数调优优化 (HPO)。超参数是在一个配置好的、受限的搜索空间中指定的,这个空间是由每个超参数在一个 config 字典中共同定义的。

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

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

  • 问题是小规模还是大规模(训练需要多长时间?资源成本如何,例如 GPU)?您能并行运行多少次试验?

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

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

如果您的模型返回增量结果(例如,深度学习中每个 epoch 的结果,GBDT 中每增加一棵树的结果等),使用提前终止通常可以采样更多的配置,因为没有希望的试验在完全运行之前就被修剪。请注意,并非所有搜索算法都能利用从修剪的试验中获得的信息。没有增量结果就无法使用提前终止——就函数式 API 而言,这意味着 tune.report() 必须被调用一次以上——通常在一个循环中。

如果您的模型很小,您通常可以尝试运行许多不同的配置。可以使用随机搜索来生成配置。您也可以对某些值进行网格搜索。如果您的模型支持提前停止,您可能仍然应该使用ASHA 进行提前终止不良试验

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

如果您拥有的超参数数量很少,贝叶斯优化方法效果很好。可以看看 BOHBOptuna 配合 ASHA 调度器,以结合贝叶斯优化和提前终止的优点。

如果您只有连续的超参数值,这对于大多数贝叶斯优化方法来说效果很好。离散或分类变量仍然可用,但随着类别数量的增加效果会变差。

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

我们的首选解决方案通常是为较小问题使用随机搜索配合 ASHA 进行提前终止。对于具有少量超参数较大问题,请使用 BOHB;对于具有大量超参数较大问题,并且可以接受学习调度,请使用 基于种群的训练 (Population Based Training)

我如何选择超参数范围?#

一个好的开始是阅读介绍算法的论文,并看看其他人是如何使用的。

大多数算法也有一些参数的合理默认值。例如,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 后通过 tune.report() 报告的内容。在一定数量的步骤后,它们会移除表现最差的试验,只保留表现最佳的试验。试验的好坏通过按目标指标(例如准确率或损失)对其进行排序来确定。

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

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

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

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

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

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

您会注意到,Ray Tune 不仅报告超参数(来自 config)或指标(传递给 tune.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 并将其包装在您的可训练对象周围,同时提供一个字典或 PlacementGroupFactory 对象。

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 工作器(因此每个 Ray Tune 试验)只能在一台机器上调度。这意味着,如果您为试验请求 2 个 GPU,但您的集群由 4 台每台只有 1 个 GPU 的机器组成,则该试验永远不会被调度。

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

在某些情况下,您的可训练对象可能希望启动其他远程 Actor,例如,如果您通过 Ray Train 利用分布式训练。在这些情况下,您可以使用放置组来请求额外的资源。

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 不一定必须与您的主可训练对象位于同一节点上。事实上,您可以通过 strategy 参数来控制这一点。在此示例中,PACK 将尝试将 Actor 调度在同一节点上,但也允许它们调度在其他节点上。请参阅放置组文档以了解这些放置策略。

您还可以通过 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 或任务无法调度?#

这通常是由可训练对象启动的 Ray Actor 或任务引起的,而可训练对象并未考虑这些资源,导致死锁。这也可以被可训练对象中使用的其他基于 Ray 的库(如 Modin)“隐秘地”引起。为了解决这个问题,请使用放置组为试验请求额外的资源,如上节所述。

例如,如果您的可训练对象正在使用 Modin DataFrame,对这些 DataFrame 的操作会产生 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()

如何将更多参数值传递给我的可训练对象?#

Ray Tune 要求您的可训练函数最多只能接受两个参数:configcheckpoint_dir。但有时您可能需要传递常量参数,例如运行的 epoch 数,或者要训练的数据集。Ray Tune 提供了一个名为 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() 也适用于类可训练对象。请参阅 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 毫秒内完成。当它持续花费更长时间时,这可能表明存在问题或效率低下。要摆脱此消息,了解其来源很重要。

这是导致此问题的主要原因:

试验配置非常大

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

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

试验结果非常大

如果您通过类可训练对象的 step() 返回值返回对象、数据或其他大型对象,或者将其返回给函数可训练对象的 tune.report(),就会发生这种情况。效果与上面相同:结果会被反复序列化并写入磁盘,这可能会花费很长时间。

解决方案:使用检查点,通过将数据写入可训练对象的当前工作目录来解决。根据您是使用类还是函数可训练 API,有多种方法可以做到这一点。

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

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

您报告结果过于频繁

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

解决方案:这里的解决方案很明显:不要太频繁地报告结果。在类可训练对象中,step() 可能应该处理更大的数据块。在函数可训练对象中,您可以只在训练循环的第 n 次迭代时报告。尝试平衡您为做出调度或搜索决策真正需要的结果数量。如果您需要更细粒度的指标来记录或跟踪,请考虑为此使用单独的记录机制,而不是 Ray Tune 提供的结果进度记录。

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

首先,请按照 构建 Ray (仅 Python) 中的说明进行操作,以在不编译 Ray 的情况下开发 Tune。Ray 设置完成后,运行 pip install -r ray/python/ray/tune/requirements-dev.txt 来安装 Tune 开发所需的所有软件包。现在,要运行所有 Tune 测试,只需运行:

pytest ray/python/ray/tune/tests/

如果您打算提交拉取请求,我们建议您事先在本地运行单元测试,以加快审查过程。尽管我们有钩子会在每次拉取请求时自动运行单元测试,但在本地运行它们通常更快,可以避免明显的错误。

如何开始为 Tune 做贡献?#

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

注意

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

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

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

如果您试图实现一定程度的可复现性,有两个地方需要设置随机种子

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

  2. 在可训练对象中(如果需要)。神经网络通常用随机数初始化,许多经典的机器学习算法,如GBDT,都使用随机性。因此,您需要确保在这里设置一个种子,以便初始化始终是相同的。

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

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(),允许您将大型对象广播到您的可训练对象。使用此包装器传递的对象将被存储在 Ray 对象存储 中,并将被自动获取并作为参数传递给您的可训练对象。

提示

如果对象大小很小或已存在于 Ray 对象存储 中,则无需使用 tune.with_parameters()。您可以改用 partial 或直接传递给 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)

请确保工作节点对云存储具有写入访问权限。否则,将会出现诸如 Error message (1): fatal error: Unable to locate credentials 之类的错误消息。对于 AWS 设置,这包括为工作节点添加 IamInstanceProfile 配置。请 在此处查看更多提示

如何在 Kubernetes 中使用 Tune?#

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

如何在 Docker 中使用 Tune?#

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

如何配置搜索空间?#

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

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。如果提供了 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 会将每个工作程序的当前工作目录更改为其对应的试验目录(例如 ~/ray_results/exp_name/trial_0000x)。这保证了每个工作程序进程都有独立的工作目录,避免了在保存特定于试验的输出时发生冲突。

您可以通过设置 RAY_CHDIR_TO_TRIAL_DIR=0 环境变量来配置此行为。这明确指示 Tune 不要将工作目录更改为试验目录,从而可以访问相对于原始工作目录的路径。一个需要注意的是,工作目录现在在工作程序之间共享,因此应该使用 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 作业。我们不测试此工作流程,并建议为每个调优作业使用单独的集群。

原因如下:

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

  2. 如果可以在您的基础设施上轻松启动新的 Ray 集群,那么运行一个大型集群通常不会比运行多个小型集群带来成本效益。例如,运行一个由 32 个实例组成的集群,成本几乎与运行 4 个由 8 个实例组成的集群相同。

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

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

请参阅 此 GitHub issue 以获取更多上下文,如果您遇到此问题,可以参考其中的解决方法。

如何继续训练一个已完成的 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()