【问题标题】:Is there a way to release the GIL for pure functions using pure python?有没有办法使用纯 python 为纯函数释放 GIL?
【发布时间】:2020-12-04 08:37:02
【问题描述】:

我想我一定是错过了什么;这看起来很正确,但我看不到这样做的方法。

假设你在 Python 中有一个纯函数:

from math import sin, cos

def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t)
    return (x, y)

是否有一些内置功能或库提供某种包装器,可以在函数执行期间释放 GIL?

在我的脑海中,我正在考虑类似的事情

from math import sin, cos
from somelib import pure

@pure
def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t)
    return (x, y)

为什么我认为这可能有用?

因为目前只对 I/O 密集型程序有吸引力的多线程,一旦这些功能长时间运行,就会变得有吸引力。做类似的事情

from math import sin, cos
from somelib import pure
from asyncio import run, gather, create_task

@pure  # releases GIL for f
async def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)
    return (x, y)


async def main():
    step_size = 0.1
    result = await gather(*[create_task(f(t / step_size))
                            for t in range(0, round(10 / step_size))])
    return result

if __name__ == "__main__":
    results = run(main())
    print(results)

当然,multiprocessing 提供了Pool.map,它可以做非常相似的事情。但是,如果函数返回非原始/复杂类型,则工作人员必须对其进行序列化,而主进程必须反序列化并创建新对象,从而创建必要的副本。对于线程,子线程传递一个指针,主线程简单地获取对象的所有权。更快(更清洁?)。

为了将此与我几周前遇到的一个实际问题联系起来:我正在做一个强化学习项目,其中涉及为类似国际象棋的游戏构建人工智能。为此,我在模拟 AI 与自己对战 > 100,000 游戏;每次返回板状态的结果序列(numpy 数组)。生成这些游戏循环运行,我每次都使用这些数据来创建更强大的 AI 版本。在这里,在主进程中为每个游戏重新创建(“malloc”)状态序列是瓶颈。我尝试重用现有对象,由于许多原因,这是一个坏主意,但这并没有带来太大的改进。

编辑:这个问题与 How to run functions in parallel? 不同,因为我不只是在寻找并行运行代码的任何方法(我知道这可以通过多种方式实现,例如通过 multiprocessing)。我正在寻找一种方法让解释器知道当这个函数在并行线程中执行时不会发生任何不好的事情。

【问题讨论】:

  • 理论上你可以通过编写一个 C 扩展来禁用 GIL。但我无法真正预测其后果。这样你就达到了不安全、未定义的行为。例如,谁知道这些 sin、cos 到底做了什么,以及在没有 GIL 的情况下它们是否能正常工作?这里的核心问题是 Python 很慢,而且它的线程支持很烂。也许你应该改用其他语言?
  • Python 是 Python,你不知道 t 是什么。如果是单个浮点数,释放和重新获取 GIL 的成本将高于 5 次三角运算和一些基本数学运算的成本。即使在没有 GIL 问题的 C++ 中,为这样的小任务创建新的 future 也是没有意义的。当然,两个 AI 之间的国际象棋游戏是另一回事,但你需要 Tensorflow。 GIL 不适用于在 GPU 上运行的代码。
  • 这能回答你的问题吗? How to run functions in parallel?
  • @freakish 我同意,不加思索地释放 GIL 就是打开潘多拉魔盒。我想到的第一个“安全”方法是创建一个新范围,该范围无权访问其执行函数的父级,然后丢弃除返回变量之外的范围。不过,我没有想到这一点,尤其是。关于imports。我认为可能已经有一个图书馆,但我不知道。
  • @MSalters 是的,我已经在使用 Tensorflow 来计算移动了。它并不能解决所有问题,原因如下:这确实是 CPU 工作。 (b) 游戏中的预测是连续的(多亏了高分支因子),(c) 你可以批处理并发运行的游戏,但是在多进程应用程序中批处理它们是痛苦的(当前方法),(d) 允许每个进程共享GPU 浪费了宝贵的 GPU 内存(权重/参数的副本)

标签: python multiprocessing python-asyncio gil


【解决方案1】:

有没有办法使用纯 python 为纯函数发布 GIL?

简而言之,答案是,因为这些功能在 GIL 运行的层面上并不是纯粹的。

GIL 不仅用于保护对象不被 Python 代码同时更新,其主要目的是防止解释器在访问和更新全局和共享数据。这包括 Python 可见的单例,例如 NoneTrueFalse,还包括所有全局变量,例如模块、共享字典和缓存。然后是它们的元数据,例如引用计数和类型对象,以及实现内部使用的共享数据。

考虑提供的纯函数:

def f(t):
    x = 16 * sin(t) ** 3
    y = 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t)
    return (x, y)

dis tool 揭示了解释器在执行函数时执行的操作:

>>> dis.dis(f)
  2           0 LOAD_CONST               1 (16)
              2 LOAD_GLOBAL              0 (sin)
              4 LOAD_FAST                0 (t)
              6 CALL_FUNCTION            1
              8 LOAD_CONST               2 (3)
             10 BINARY_POWER
             12 BINARY_MULTIPLY
             14 STORE_FAST               1 (x)
             ...

要运行代码,解释器必须访问全局符号 sincos 才能调用它们。它访问整数 2、3、4、5、13 和 16,它们都是 cached,因此也是全局的。如果发生错误,它会查找异常类以实例化适当的异常。即使这些全局访问不修改对象,它们仍然涉及写入,因为它们必须更新reference counts

在没有同步的情况下,这些都不能从多个线程安全地完成。虽然可以想象修改 Python 解释器以实现不访问全局状态的真正纯函数,但它需要对内部进行重大修改,影响与现有 C 扩展的兼容性,包括广受欢迎的科学扩展。最后一点是事实证明移除 GIL 如此困难的主要原因。

【讨论】:

    猜你喜欢
    • 2017-05-22
    • 1970-01-01
    • 2021-06-21
    • 2014-10-29
    • 1970-01-01
    • 2022-11-02
    • 2016-02-09
    • 1970-01-01
    • 2020-04-13
    相关资源
    最近更新 更多