Scikit-learn: RFC 我们应该如何控制/公开基于 OpenMP 的并行 cython 代码的线程数?

创建于 2019-07-05  ·  58评论  ·  资料来源: scikit-learn/scikit-learn

在添加基于 OpenMP 的并行性之前,我们需要决定如何控制线程数以及如何在公共 API 中公开它。

我看到了来自不同人的几个提议:

(1)使用现有的n_jobs公共参数, None表示 1(对于 joblib 并行性相同)

(2)使用现有的n_jobs公共参数, None表示-1(就像 numpy 让 BLAS 使用尽可能多的线程)

(3)当底层并行由 OpenMP 处理时,增加一个新的公共参数n_omp_threadsNone表示 1。

(4)当底层并行由OpenMP处理时,增加一个新的公共参数n_omp_threadsNone表示-1。

(5)不要在公共 API 中公开它。 使用尽可能多的线程。 用户仍然可以在运行前使用OMP_NUM_THREADS或在运行时使用 threadpoolctl 进行一些控制。

(1) 或 (2) 将需要改进每个估计器的n_jobs文档:什么是默认值,什么样的并行性,并行完成什么......(参见 #14228)

@scikit-learn/core-devs,您更喜欢哪种解决方案?
如果不是以前的,你的解决方案是什么?

High Priority

最有用的评论

仅供参考:joblib 0.14.0(对于 Python 进程与本机线程池的情况具有固定版本的覆盖)已发布: https ://pypi.org/project/joblib/0.14.0/

所有58条评论

我的偏好是(2)。 我朝那个方向打开了#14196

感谢您打开问题。

我偏爱(4)。 n_omp_threads的默认值可以是'auto',这意味着“使用尽可能多的线程,避免过度订阅”

我不喜欢(2),因为:

  • 相同的参数用于不同的事物(OpenMP 与 joblib)。
  • 根据底层实现,默认值会有所不同。 如果我们要将实现公开给用户,我更喜欢(4),因为它更明确。
  • 很难向用户解释(n_jobs 的词汇表已经很丰富了),尤其是对于同时使用 joblibopenmp(如果有的话)的估计器。

话虽如此,我不会投票-1。

我倾向于 3 或 4。我不确定哪个更好,但正如@NicolasHug所说,我也希望它与n_jobs不同。

我也宁愿让它与 n_jobs 不同。

我认为命名 openMP 并行性与 Python 级别不同
并行性是一个非常技术性的细微差别,我们对我们的大部分
用户。 我们需要能够给出一个简单的信息来说明如何
控制并行度。

关于默认值,我也希望有正确的默认值
这可以防止超额认购。 我们是否有所需的技术工具
那? 我知道 Jeremie 和 Olivier 一直在苦苦挣扎
超额认购。

我已经回顾了在其他一些使用 OpenMP 的 ML 库中所做的事情,

  • xbgoost 有 nthread,但在他们的 scikit-learn API 中弃用了它以支持n_jobs
  • lightgbm 使用n_jobs
  • 在 recsys lightfm 和隐式使用num_threads

我猜num_threads在语义上会更正确,但我主要关心的是,

  • 许多模型的附加参数
  • 我们如何处理并行性变化。 假设现在我们有一个使用多处理的纯 python 代码,我们将它重写为 Cython + OpenMP,如果我们使用 (4) 将需要弃用警告,并更改参数名称,我不认为向用户提出这样的要求是合理的(或者我们应该为此付出努力)。
  • 同样,带有线程的 joblib(假设它在释放 GIL 的函数上运行)与使用 OpenMP 非常相似。 在这种情况下使用不同的参数名称不是很合乎逻辑,但同时我们也不希望在许多估计器中重命名参数名称。

所以我在这里没有完美的解决方案,但我也更赞成2。

(1) 或 (2) 将需要改进每个估计器的 n_jobs 文档:[...] 什么样的并行性,并行完成的工作......

我认为不应该记录在案。 这是一个可以随时更改的实现细节,大多数人只关心当增加 n_jobs 时它会使他们的代码更快(如果不这样做会感到失望)。

我认为不应该记录在案。

更准确地说,我认为我们应该有一个文档部分/词汇表来讨论不同的可能并行级别,但不应该在每个估算器中指定它。

我知道它们可以更改,我们不向用户保证它们会保持不变,但是就在集群上分配作业或有时使用 dask 的后端而言,这对用户和开发人员来说要容易得多了解哪些部分在 MPI 上,哪些部分在流程上。

例如,在集群上分配作业,或者有时使用 dask 的后端时,用户和开发人员更容易知道哪些部分在 MPI 上,哪些部分在进程上。

如果一个人离开, n_jobs=None OMP_NUM_THREADS并使用with joblib.parallel_backend('dask')让 dask 处理数量或进程/节点不应该独立于实现工作吗?

我们已经到处都在发生 BLAS 并行性,因此当在顶部添加 OpenMP 和进程时,在如此复杂的系统中,最简单的方法就是进行基准测试,看看在线程/进程数量方面什么效果最好。 假设我们在某处添加了 OpenMP,这可能会或可能不会值得注意,具体取决于之前在 BLAS 中发生的 linalg 操作的数量(该比率也可能随着数据大小而以某种未知的功率缩放)。

我也希望有正确的默认值来防止超额订阅。 我们是否拥有为此所需的技术工具?

这个想法是在 prange 之前在估计器属性n_jobsn_threads $ 上调用openmp_effective_n_threads 。 它将返回一个取决于omp_get_max_threads的值。 我们可以在 python 级别使用 threadpoolctl 对其进行影响,以防止超额订阅(例如,我们可以在网格搜索中使用它)。

我知道 Jeremie 和 Olivier 一直在为超额认购而苦苦挣扎。

它适用于所有 sklearn 用例:)

我想更多地了解超额认购。 @jeremiedbb为什么它可以正常工作,这是什么意思?
如果我用n_jobs=-1运行GridSearchCV $ 并且在里面我有一个使用 OpenMP 的估计器并且我设置n_jobs=-1 ,会发生什么?

网格搜索不是一个很好的例子,因为 joblib 应该已经处理了这种情况。 它通过将 openmp 的线程数设置为 1 来实现。

@jeremiedbb为什么这不是一个好例子? 如果用户在GridSearchCVn_jobs=-1和 OpenMP 估计器中设置n_job=2 ? 或者两者都只有 4 个内核的n_jobs=4
抱歉,我不熟悉 joblib 如何更改 openmp 线程。

假设我们可以确保对 joblib 中的订阅提供良好的保护(我相信很快就会出现这种情况),我会赞成设置默认值,让我们的 OpenMP 循环使用当前最大线程数(就像 OpenBLAS 和 MKL 一样numpy / scipy)。

+1 让用户根据需要使用n_jobs参数传递明确数量的线程。 只是对于n_jobs=None (默认值),我会让 OpenMP 做它想做的事情。

对于基于进程的并行性(例如,使用 joblib 的 loky 后端),我认为默认情况下保持默认的n_jobs=None意味着顺序更安全,因为可能存在不平凡的通信和内存开销。

对于带有线程后端的 joblib,我倾向于保留当前行为,但如果有必要,稍后重新探索这个选择。

作为参考,joblib 中的超额订阅保护将由https://github.com/joblib/joblib/issues/880跟踪。

我的口袋在我完成之前寄了这个:D

这不是 threadpoolctl 的好例子。 但它适用于joblib。 虽然我们发现它在以前的 joblib 版本中不起作用,但它应该在下一个版本中修复。

执行嵌套并行 openmp/blas 时将使用 Threadpoolctl。 它允许通过限制 blas 的线程数来防止超额订阅。 这种情况现在只发生在 kmeans 上。

总的来说,我们通过禁用非顶级并行性的并行性来防止超额订阅。

总之,任何默认值都适用于 n_jobs,因为当它已经在并行调用中时,它会被强制为 1

总之,任何默认值都适用于 n_jobs,因为当它已经在并行调用中时,它会被强制为 1

只是为了让我明白这一点:如果我在 HistGradientBoosting 或 OpenMP 上运行网格搜索,我们可能希望默认它使用所有可用的内核。 现在用户在GridSearchCV中设置n_jobs=2 $ 以使事情运行得更快,但说他们有超过 2 个内核。 GridSearchCV将使用 joblib 进行并行化,但 joblib 会阻止并行调用中的 OpenMP 并行性,因此活动的线程会更少,事情可能会更慢。

单独的场景:
有人在GridSearchCVHistGradientBoosting n_jobs 无论如何,用户明确设置的HistGradientBoosting将被忽略,对吗?

现在用户在 GridSearchCV 中设置 n_jobs=2 以使事情运行得更快,但说他们有超过 2 个核心。 GridSearchCV 将使用 joblib 进行并行化,但 joblib 会阻止并行调用中的 OpenMP 并行性,因此活动的线程会更少,事情可能会更慢。

那就对了。 在这种情况下,他应该为网格搜索设置 n_jobs=-1 以从所有内核中受益。 在许多情况下(个人观察),最好在最外层循环中启用并行性。

请注意,BLAS 就是这种情况。 如果为估算器设置 n_jobs=2,joblib 并行化,则 BLAS 将被限制为仅使用 1 个核心。

如果我们正确记录它,我认为这种行为是可以的。

在这种情况下,他应该为网格搜索设置 n_jobs=-1 以从所有内核中受益。

好的,但是如果他们从 n_jobs=1 变为 2,他们会得到更糟糕的结果,这可能有点出乎意料。

请注意,BLAS 就是这种情况。 如果为估算器设置 n_jobs=2,joblib 并行化,则 BLAS 将被限制为仅使用 1 个核心。

这不是在创建的进程中通过OMP_NUM_THREADS设置的吗? 这意味着它可能是任何东西,包括available_cpu_cores // n_jobs

请注意,轻微的超额订阅可能还不错(至少上次我在 HPC 中研究它时)。 例如,如果您有 4 个 CPU 内核,如果任务使用异构计算单元,则 2 个进程各 4 个线程不一定比 2 个进程各 2 个线程差; 问题实际上出在具有 40² 线程的 40 核 CPU 上。

有人在 GridSearchCV 和 HistGradientBoosting 中设置了 n_jobs。 无论如何,用户明确设置的 HistGradientBoosting 将被忽略,对吧?

所以基本上,它是关于OMP_NUM_THREADS和用户提供的num_threads (或n_jobs对应于线程)之间的优先级。 如果提供了一个不是None num_threads也许它应该比OMP_NUM_THREADS具有更高的优先级?

好的,但是如果他们从 n_jobs=1 变为 2,他们会得到更糟糕的结果,这可能有点出乎意料。

我同意。 这是默认 None 表示 -1 的一个缺点。 但我认为这也是一个文档问题,因为如果我知道我的估算器已经使用了所有核心,我为什么要增加网格搜索的工作人员数量? 或者也许我想禁用我的估计器的并行性并使用所有核心进行网格搜索(这是你的下一点)。

这不是在创建的进程中通过 OMP_NUM_THREADS 设置的吗?

是的。

这意味着它可能是任何东西,包括 available_cpu_cores // n_jobs?

我想这是可能的。

请注意,轻微的超额订阅可能还不错(至少上次我在 HPC 中研究它时)。 例如,如果您有 4 个 CPU 内核,如果任务使用异构计算单元,则 2 个进程各 4 个线程不一定比 2 个进程各 2 个线程差; 问题实际上出在具有 40² 线程的 40 核 CPU 上。

确实,但这也不好(至少我从未经历过通过超额订阅获得更好的性能)。

如果提供了一个不是 None 的 num_threads,也许它应该比 OMP_NUM_THREADS 具有更高的优先级?

在我建议的实现中,n_jobs/num_threads 优先于环境变量。 只有默认值(无)会受到环境变量的影响。 如果您使用 n_jobs=2 进行网格搜索并设置 n_jobs/n_threads=(n_cores // 2),您的核心将完全饱和。

我要补充一点,拥有良好的默认值很重要,但它不应该阻止用户思考这些默认值是什么,它们为什么好,以及它们什么时候好。

我同意。 这是默认 None 表示 -1 的一个缺点。 但我认为这也是一个文档问题,因为如果我知道我的估算器已经使用了所有核心,我为什么要增加网格搜索的工作人员数量?

每个进程启动一定数量的线程的多处理的情况让我想到 HPC 中的混合 MPI/OpenMP 编程(参见例如这个演示文稿)。 这个类比可能有点片面,因为我们既不使用 MPI 也不在多个节点上运行,但是(目前)启动新进程时的数据序列化成本确实与使用 MPI 的通信成本有类似的影响。
据我所知,HPC 领域的结论是进程/嵌套线程的最佳数量取决于应用程序和硬件。

所以我的观点是,能够分别控制数量或并行进程以及嵌套线程的数量可能很有用,但要控制后者,最好有一些全局机制,例如OMP_NUM_THREADS (例如通过 joblib 设置),它也适用于 BLAS,而不是num_threads参数,它只适用于 scikit-learn 特定的 OpenMP 循环。

这是默认 None 表示 -1 的一个缺点。

我不认为“无”应该意味着“-1”或“1”。 应该是“最好的
在给定全局约束的情况下猜测是有效的”,以及
这可以演变并取决于上下文。

我的意思是,当用户指定“无”时,我们应该作为时间
去添加动态调度逻辑以提高效率,我们应该
明确告诉用户工作的细节实现
调度将发展。

Note that slight oversubscription may not be bad (at least last time I
looked into it in HPC). e.g. if you have 4 CPU cores, 2 processes with 4
threads each is not necessarily worse than 2 process with 2 threads each if
the task use heterogeneous compute units; the problem is really on 40 core
CPU with 40² threads.

是的,它符合我的经验,只要我们不破坏记忆。

请注意,BLAS 就是这种情况。 如果为估算器设置 n_jobs=2,joblib 并行化,则 BLAS 将被限制为仅使用 1 个核心。

我没有意识到这一点! 那是……一个有趣的副作用。 总是这样吗? 这在任何地方都有记录吗?

它已在 joblib 0.12 中引入,请参阅更改日志
它记录在这里,在“避免 CPU 资源的过度订阅”下

但是,直到现在它一直存在错误,应该在下一个 joblib 版本中修复。

那么这是 scikit-learn 0.20 中引入的更改,但未记录在更改日志中? 我们是否以某种方式向 scikit-learn 用户传达了这一变化?

无论如何,听起来我们默认需要不同的策略,比如available_cpu_cores // n_jobs对吗?

那么这是 scikit-learn 0.20 中引入的更改,但未记录在更改日志中? 我们是否以某种方式向 scikit-learn 用户传达了这一变化?

好吧,既然我们取消了 joblib,我不确定在 scikit-learn 发行说明中可以在哪里交流有关 joblib 并行性更改的信息。

没关系,如上所述,此功能已添加但实际上由于错误而没有效果,因此无需担心。

无论如何,听起来我们默认需要不同的策略,比如 available_cpu_cores // n_jobs 对吗?

https://github.com/joblib/joblib/pull/913中提出的更改

另外我认为我们仍然应该合并https://github.com/scikit-learn/scikit-learn/pull/14196 :它是一个能够安全使用n_jobs or n_threads=-1的私有函数,并且与目前的讨论。

无论如何,听起来我们默认需要不同的策略,比如 available_cpu_cores // n_jobs 对吗?

它不起作用,因为您可以嵌套joblib.Parallel调用。

此外,我不认为这是一个糟糕的行为。 目前n_jobs与您实际使用的内核数量不匹配,因为它不会影响 BLAS。 用户经常感到惊讶,因为您要求 4 个工作,而您的 40 核机器上的 htop 已满。

讨论已经衍生到 sklearn 中的超额认购问题,这并不是最初的目的,我认为可以单独处理,假设我们有正确的工具来处理独立于默认值的超额认购。 因此,我建议将讨论重新集中在最初的问题上。

让我试着总结一下讨论。
我们目前在使用 n_jobs 和 default = 让 OpenMP 尽可能多地使用添加一个具有相同 default 的新参数n_openmp_threads之间几乎平分秋色。

一件好事是我们似乎同意默认值:)

关于默认值选择的评论:

  • 这种默认选择受到以下事实的支持:有多种方法可以告诉 OpenMP使用尽可能多的方法。 通过OMP_MAX_THREADS env var 或通过threadpoolctl ,我们可以在 sklearn 的嵌套并行情况中使用它们。
  • joblib 将 openmp max threads 设置为 1 的事实并不完全令人满意。 我们可以将最大线程数作为参数传递给joblib.Parallel

关于参数名称的注释:

  • 保留n_jobs的优点:

    • 为并行性保留一个参数

    • 将底层实现从 joblib 迁移到基于 OpenMP 的并行性会导致参数名称和弃用周期的变化。

  • 更改名称的优点:

    • 根据估算器具有不同默认值的相同参数名称令人困惑。

    • 对不同的事物使用不同的名称

我建议尝试多讨论一点,看看一方是否设法说服另一方:)
那么如果我们仍然不同意,我想我得睡一觉了?

joblib 将 openmp max threads 设置为 1 的事实并不完全令人满意。

我会争辩说,这是我不注意时在 joblib 中引入的一个错误。 它对使用 BLAS 的任何东西都有可怕的性能后果,对梯度提升也没有。
此外,这是在某些时候做出的向后不兼容的更改,甚至没有在 whatsnew 中提及,即许多人的代码变得非常慢。

我发现您的“参数名称”摘要很难理解,因为没有完全遵循这个线程。 您的意思是使用n_jobs名称来设置线程数吗? 这似乎很可怕。 并且具有不同的默认值似乎也很糟糕。 有同时做多处理和多线程的估计器(我假设是kmeans?)。 参数在那里做什么?

我不明白为什么我们会让用户通过参数处理线程池。
为什么用户要关心我们是通过 Cython 还是 BLAS 使用多线程? 每次调用np.dot时,我们都没有设置OMP_MAX_THREADS的参数。 那么我们为什么要把它放在其他地方呢?

我会争辩说,这是我不注意时在 joblib 中引入的一个错误。 它对使用 BLAS 的任何东西都有可怕的性能后果,对梯度提升也没有。

我们认为我们已经在 joblib 0.12 中实现了超额订阅保护(在具有嵌套 OpenMP/BLAS 的 Python 工作进程的情况下),但实际上并非如此,因为工作进程的启动方式和它们的运行时初始化(更多细节在 joblib/作业库#880)。

@tomMoral最近一直致力于改进 loky 启动其工作进程以获得更多控制的方式 (tomMoral/loky#217),这已于昨天在 loky 2.6.0 中发布。 下一步是让 joblib 使用此基础结构将每个工作进程中允许的默认线程数设置为 cpu_count / number_of_worker 进程:joblib/joblib#913。 用户可以通过以下方式覆盖此默认行为:

from joblib import parallel_backend

with parallel_backend("loky", inner_max_num_threads=2):
    # sklearn code using joblib and OpenMP/BLAS goes here

此外,这是在某些时候做出的向后不兼容的更改,甚至没有在 whatsnew 中提及,即许多人的代码变得非常慢。

它实际上在 joblib 的变更日志https://github.com/joblib/joblib/blob/master/CHANGES.rst#release -012 中提到,但我同意它不够明确。 我们将确保在下一个 joblib 版本的变更日志中更加明确。

我不明白为什么我们会让用户通过参数处理线程池。
为什么用户要关心我们是通过 Cython 还是 BLAS 使用多线程? 每次调用 np.dot 时,我们都没有设置 OMP_MAX_THREADS 的参数。 那么我们为什么要把它放在其他地方呢?

我可以不暴露 scikt-learn API 中的任何参数来控制 OpenMP/BLAS 线程,而不是重载 n_jobs 的含义。

我不明白为什么我们会让用户通过参数处理线程池。
为什么用户要关心我们是通过 Cython 还是 BLAS 使用多线程? 每次调用 np.dot 时,我们都没有设置 OMP_MAX_THREADS 的参数。 那么我们为什么要把它放在其他地方呢?

这正是该线程顶部提出的第 5 个解决方案,但似乎没有人喜欢它,所以我没有在摘要中提及它。 我同意这是一个合法的解决方案,可能值得更多关注:)

@jeremiedbb抱歉,我应该从头开始;)

选项 5) 是唯一一个似乎与当前行为一致的选项。 不过还是没看懂你的总结...

@ogrisel关键是 sklearn 的行为发生了巨大变化,因此应该在 sklearn 更改日志中记录,对吗? 至少只要我们在销售 joblib?

我想我对 1-4 的问题是“当底层并行性由 OpenMP 处理时”是什么意思。 我们是否在检查我们使用的 blas 是否是使用 OpenMP 构建的,如果是,请检查函数中是否有任何对 blas 的调用?

我想我对 1-4 的问题是“当底层并行性由 OpenMP 处理时”是什么意思。 我们是否在检查我们使用的 blas 是否是使用 OpenMP 构建的,如果是,请检查函数中是否有任何对 blas 的调用?

不,这意味着我们基于 OpenMP 的 cython 代码。 假设您有一个具有并行实现(基于 OpenMP)的估计器,例如 HGBT。 问题是我们想要以下哪种解决方案?

  • HGBT应该有一个n_jobs参数,默认使用尽可能多的核
  • HGBT 应该有一个n_jobs参数,默认为单线程
  • HGBT应该有一个n_threads参数,默认使用尽可能多的核心
  • HGBT 应该有一个n_threads参数,默认为单线程
  • HGBT不应该有任何参数,它使用尽可能多的核心,仅此而已

我们都同意默认情况下单线程不是一个好的选择。 所以我们必须在 1、3 和 5 之间做出选择。我希望这个总结更清楚:)

不,这意味着我们基于 OpenMP 的 cython 代码

但是为什么用户需要关心我们是否编写了 cython 代码或者我们正在调用 blas? 这与用户有什么关系?

如果我们用我们自己的基于 OpenMP 的实现替换对dot的调用,那么多线程方面的行为不会改变(或者我可能遗漏了什么?)但是因为我们编写了代码,所以我们将提供不同的界面?

我同意第五个解决方案更一致。 我们让 BLAS 做它想做的事,我们应该对基于 OpenMP 的多线程做同样的事情。

但是有可能对线程数量进行一些控制是很好的,特别是对于 OpenMP 并行性不在算法的一小部分而是在我的 KMeans PR 中的最外层循环中的估计器。

我们让 BLAS 做它想做的事,我们应该对基于 OpenMP 的多线程做同样的事情

再说一次,你看这种方式比我看的多,所以我可能会遗漏一些东西。 但是 OpenBLAS(可选?)在底层使用 OpenMP,对吗?

numpy 和 scipy 附带的 OpenBLAS 没有(但您可以构建它以使用 OpenMP 并链接 numpy)。 MKL 使用 OpenMP。

但是为什么用户需要关心我们是否编写了 cython 代码或者我们正在调用 blas? 这与用户有什么关系?

我完全同意。 对于使用 OpenMP 的少量代码,这是最有意义的。

我的动机是像 KMeans 这样的估计器,其并行性发生在最外层循环中。

行。 所以这就是为什么我觉得“基于 OpenMP 的多线程”这个短语非常令人困惑。 因为您真正的意思是“OpenMP 基础多线程,我们在 Cython 中将调用写入 OpenMP”。

也许在 KMeans 中使用 OpenMP 的方式与在 Nystroem 中使用 OpenMP 的方式存在质的差异,但至少对我来说这并不明显(抱歉,如果上面已经讨论过)。

你能多说一点关于这个区别吗?

我不能我不知道它将如何在 Nystroem 中使用:/

这个想法是,如果它是一些并行的代码(在我们的 cython 代码中使用 OpenMP),我们希望它表现得像 BLAS,即使用尽可能多的内核。 另一方面,如果整个算法是并行的(在我们的 cython 代码中仍然是 OpenMP),那么在最外层的循环中,也许我们想提供一些控制。

Nystroem 只是对 SVD 的调用,我认为它完全由 blas 处理;)

我还认为with parallel_backend("loky", inner_max_num_threads=2):不暴露 scikit-learn 中的线程控制是个好主意。 这将回避整个n_jobs / n_threads讨论,并让我们有可能稍后在需要时对其进行更改,同时仍然为高级用户提供控制 n_threads 的机制。

每次调用 np.dot 时,我们都没有设置 OMP_MAX_THREADS 的参数。 那么我们为什么要把它放在其他地方呢?

作为旁注,关于默认使用所有线程,例如在 BLAS 中,逻辑可能起源于 10-20 年前,当时计算机有 2-4 个内核。 现在,您可以获得具有 10-20 个 CPU 内核的桌面和多达 100 个 CPU 内核的服务器。在这种情况下,默认使用所有内核假设我们的库在世界上是独一无二的,并且可以免费使用所有资源。 在很多情况下,使用所有 CPU 内核并不理想(共享服务器,甚至是台式机/笔记本电脑以及运行其他资源密集型应用程序,例如在浏览器中渲染所有 JS)。 在负载下跨越所有线程的服务器上,由于过度订阅,实际上会减慢正在运行的应用程序和其他应用程序。

此外,没有多少应用程序能够很好地扩展超过 10 个 CPU 内核(或在有意义的情况下使用足够大的数据)是的,使用线程几乎总是更快,但代价是 CPU 时间和电力消耗显着增加。 也许我们不太关心 scikit-learn 用例,但它仍然值得考虑。 并不是说我们现在应该改变提议的方法中的任何内容,而是让事情足够灵活,以便我们以后可以在需要时更改此默认值。

装饰师看起来很合理。
虽然如果这不适用于 BLAS,我相信我们会收到困惑的用户提出的问题,这基本上意味着有两种方法可以控制线程数,对吧?

附带说明一下,默认情况下使用所有线程,例如在 BLAS 中,逻辑可能起源于 10-20 年前,当时计算机有 2-4 个内核 [...] 并不是很多应用程序具有超过 10 个 CPU 内核的良好扩展性

我同意。 根据我们的经验,在大型计算机上默认扩展到所有 CPU 是一个坏主意,原因有很多:

  • 当多个进程像这样运行时(这在 Python 中经常发生),它会导致巨大的超额订阅,甚至会冻结盒子。 一个例子是 n_job=-1 + openMP 使用所有线程,在一个有 n 个 CPU 的机器上,它使用 n**2 个 CPU,如果 n 很大,这会导致灾难

  • 这些盒子通常是多租户的。

我赞成默认情况下内部线程数不应超过 10。

全局问题是一个难题:在一个真实的用户代码库中,如今,我们最终得到了多个嵌套的并行计算系统:Python 的多处理、Python 的线程、openMP 的并行计算(如果 scikit- learn 是用 GCC 编译的,MKL 是用 ICC 编译的)。

我们真正需要的是资源的动态调度。 顺便说一下,英特尔的 TBB 提供了这一点(尽管我不建议我们使用它:D)。 这是一个难题,我们不会在此时此地解决它。 但是,最好考虑一下我们在并行计算方面给用户的合同中这个方向的可能演变。

随着joblib的下一个版本(应该很快就会发生),应该减少超额订阅问题。 它将提供两个改进:

  • 默认情况下,子进程中 BLAS 的max_num_threads将设置为cpu_count() // n_jobs ,这是避免过度订阅的合理值。 这应该解决主要问题,即当numpy用于在大型集群上使用n_jobs=-1的估计器中时,这会导致灾难性的超额订阅,而对于较少数量的n_job仍然保持高性能

  • 如果高级用户需要,可以使用parallel_backend上下文管理器来参数化这个数字以设置不同的值。 与使用全局环境变量(如OMP_NUM_THREADS )相比,这将为用户提供更好的控制,这些变量也会降低主进程的性能。

with parallel_backend('loky', inner_max_num_threads=2):
    # do stuff, the BLAS in child processes will use 2 threads.

虽然如果这不适用于 BLAS,我相信我们会收到困惑的用户提出的问题,这基本上意味着有两种方法可以控制线程数,对吧?

这也适用于 BLAS。 目前我们计划设置 MKL_NUM_THREADS、BLIS_NUM_THREADS、OPENBLAS_NUM_THREADS 和 OMP_NUM_THREADS。

@ogrisel抱歉,我不确定我是否理解您的说法。
什么适用于什么?
您是说 loky 会设置这些环境变量,对吧? 但是,如果用户已经在脚本或命令行中设置了这些变量,或者他们在集群上启动了作业怎么办?

那么在 loky 或 sklearn 中是否会强制执行默认值(例如 10)?

@amueller @NicolasHug我们刚刚在joblib:master中合并了这个超额订阅缓解功能,你能试试看它是否符合你的预期吗? 您可以在joblib 文档中找到有关此新功能的一些说明。

@ogrisel抱歉,我不确定我是否理解您的说法。
什么适用于什么?
您是说 loky 会设置这些环境变量,对吧? 但是,如果用户已经在脚本或命令行中设置了这些变量,或者他们在集群上启动了作业怎么办?

那么在 loky 或 sklearn 中是否会强制执行默认值(例如 10)?

当前实现的行为给出:

  • 如果用户什么都不做并使用n_jobs并行进程进行计算,则对具有内部线程池(OpenBLAS、MKL、OpenMP ..)的本机库的调用将被限制为cpu_count // n_jobs默认值)。
  • 如果用户设置了诸如*_NUM_THREADS=n_thread之类的环境变量,则此限制会在子进程中传递,我们不会更改行为。 这主要用于用户在整个程序中禁用 BLAS 调用的并行处理的情况,因此使用n_thread=1
  • 对于更高级的使用,用户可以使用上下文with parallel_backend('loky', inner_max_num_threads=n_thread):以编程方式覆盖它,这将优先于其他行为,并可用于在本地设置资源分配。

您可以在 joblib 文档中找到有关此新功能的一些说明。

@tomMoral看起来很棒!只是一个问题,假设我想限制 BLAS 线程的数量而不使用任何其他并行性。可以使with joblib.parallel_backend(None, inner_max_num_threads=n_thread)类的东西起作用,还是在这种情况下我应该直接使用出售的 threadpoolctl?如果它有一个单一的 API,那就太好了。

另外我猜如果你用joblib明确地创建基于线程的并行性,那些不会受到限制?或者他们会吗?

如果要限制进程中的线程数,则需要在启动进程之前设置环境变量*_NUM_THREADS或使用threadpoolctl 。 目前,我们只依赖前者,因为threadpoolctl仍然存在一些重大问题。 特别是,它非常不可靠。 由于多个本机库与线程池之间的不良交互,我们发现了多种使用它来创建死锁的方法。

实际上,对于基于线程的并行性,我们没有解决方案来设置每个线程中的内部线程数,除了 env 变量的全局限制。

目前,我们只依赖前者,因为 threadpoolctl 仍然存在一些主要问题。 特别是,它非常不可靠。 由于多个本机库与线程池之间的不良交互,我们发现了多种使用它来创建死锁的方法。

更具体地说,在某些情况下,对于同时与多个 openmp 运行时链接的程序,动态内省或更改线程池的大小似乎是有问题的。 例如这里有一个正在调查的案例: https ://github.com/joblib/threadpoolctl/issues/40

所以现在我宁愿尽可能少地使用 threadpoolctl,直到我们更好地理解上述死锁的原因。 很可能是我们在那些 openmp 运行时中发现了线程安全错误,如果是这种情况,我们将报告时间以便可以在上游修复它们。

同时控制线程数的安全方法:

  • 在启动主进程之前或在启动工作进程之前设置环境变量(就像我们现在在 joblib master 中为子进程所做的那样)。

  • 另一种选择是让我们的(scikit-learn)Cython 并行循环显式地将显式 int 值传递给“num_threads”参数。 scikit-learn 可以维护一个默认使用的全局变量(它应该是线程本地的吗?),并且可以全局设置。 或者,我们可以在每个估计器的基础上公开n_jobs样式参数,或者同时公开两者。

仅供参考:joblib 0.14.0(对于 Python 进程与本机线程池的情况具有固定版本的覆盖)已发布: https ://pypi.org/project/joblib/0.14.0/

好的,所以我目前的理解如下:

  • joblib.Parallel会将生成的子进程的OMP_NUM_THREADS变量设置为默认情况下避免过度订阅的值
  • 用户仍然可以通过设置OMP_NUM_THREADS (优先)或使用joblib.Parallel作为上下文管理器并传递inner_max_num_threads来控制 openMP 线程的数量

那是对的吗?

如果是这样,我完全支持选项 5(不公开任何内容),当然在用户指南的某处正确记录了这一点。

有一个限制。 它不包括线程后端。 这就是我们致力于 threadpoolctl 的原因。

根据上次会议,对于我们想要介绍pranges的大多数地方,选项 5 被认为是首选选项。

所以我想我可以结束这个讨论,我们可以为特定的估计器(如 hgbt 或 kmeans)打开新的讨论。 如果您不同意,请随时重新打开。

我会尽快打开 PR 以获取文档

此页面是否有帮助?
0 / 5 - 0 等级