【问题标题】:Loading pretrained glove on production with flask and Gunicorn使用烧瓶和 Gunicorn 在生产中加载预训练手套
【发布时间】:2020-08-31 07:50:56
【问题描述】:

我有一个模型需要使用来自斯坦福的 Glove 进行一些预处理。根据我的经验,此代码加载 Glove 至少需要 20-30 秒:

glove_pd = pd.read_csv(embed_path+'/glove.6B.300d.txt', sep=" ", quoting=3, header=None, index_col=0)
glove = {key: val.values for key, val in glove_pd.T.items()}

我的问题是在生产应用中处理此问题的最佳做法是什么?据我所知,每次我重新启动服务器时,我都需要等待 30 秒,直到端点准备好。

另外,I have read 在使用 Gunicorn 时,建议使用 workers>1 运行,类似这样:

ExecStart=/path/to/gunicorn --workers 3 --bind unix:app.sock -m 007 wsgi:app

这是否意味着每个 gunicorn 实例都需要将相同的手套加载到内存中?这意味着服务器资源会非常大,如果我在这里告诉我。

我的问题是,在生产服务器上托管需要预训练嵌入 (glove/word2vec/fasttext) 的模型的推荐方法是什么

【问题讨论】:

    标签: python stanford-nlp word2vec fasttext


    【解决方案1】:

    在某一层面上,如果您需要它在内存中,这就是将千兆字节以上从磁盘读取到有用的 RAM 结构所需的时间,那么是的 - 这就是进程准备好使用该数据所需的时间.但是还有优化的空间!

    例如,将其作为第一个 Pandas 数据帧读取,然后将其转换为 Python 字典,比其他选项涉及更多的步骤和更多的 RAM。 (在瞬间高峰,当glove_pdglove 都被完全构造和引用时,你将在内存中有两个完整的副本——而且都没有理想的紧凑,这可能会引发其他减速,特别是如果膨胀触发使用任何虚拟内存。)

    正如您所担心的,如果 3 个 gunicorn 工作人员各自运行相同的加载代码,则会加载相同数据的 3 个单独副本 - 但有一种方法可以避免这种情况,如下所示。

    我建议首先将向量加载到用于访问词向量的实用程序类中,例如 Gensim 库中的 KeyedVectors 接口。它将所有向量存储在一个紧凑的 numpy 矩阵中,并带有一个类似 dict 的接口,仍然为每个单独的向量返回一个 numpy ndarray

    例如,您可以将 GLoVe 文本格式向量转换为稍微不同的交换格式(带有一个额外的标题行,Gensim 在原始 Google word2vec.c 代码使用它后调用 word2vec_format)。在gensim-3.8.3(截至 2020 年 8 月的当前版本)中,您可以:

    from gensim.scripts.glove2word2vec import glove2word2vec
    glove2word2vec('glove.6B.300d.txt', 'glove.6B.300d.w2vtxt')
    

    然后,实用程序类KeyedVectors 可以像这样加载它们:

    from gensim.models import KeyedVectors
    glove_kv = KeyedVectors.load_word2vec_format('glove.6B.300d.w2vtxt', binary=False)
    

    (从未来的gensim-4.0.0 版本开始,应该可以跳过转换& 只需使用新的no_header 参数直接读取GLoVe 文本文件:glove_kv = KeyedVectors.load_word2vec_format('glove.6B.300d.w2vtxt', binary=False, no_header=True)。但是这种无标题格式会有点速度较慢,因为它需要两次遍历文件 - 第一次学习完整大小。)

    只加载一次到KeyedVectors 应该已经比原来的通用两步过程更快、更紧凑。而且,类似于您在先前的字典中所做的查找将在 glove_kv 实例上可用。 (此外,还有许多其他便利操作,例如排名.most_similar() 查找,它们利用高效的数组库函数来提高速度。)

    不过,您可以采取另一个步骤,最大限度地减少加载时解析,并延迟加载整个向量集的不需要范围,并在进程之间自动重用原始数组数据。

    额外的步骤是使用 Gensim 实例的 .save() 函数重新保存向量,该函数会将原始向量转储到一个单独的密集文件中,该文件适用于下次加载时的内存映射。所以首先:

    glove_kv.save('glove.6B.300d.gs')
    

    这将创建多个文件,如果重新定位,这些文件必须保存在一起 - 但保存的 .npy 文件将是内存映射准备好的最小格式。

    然后,稍后需要时,加载为:

    glove_kv = KeyedVectors.load('glove.6B.300d.gs', mmap='r')
    

    mmap 参数使用底层操作系统机制将相关矩阵地址空间简单地映射到磁盘上的(只读)文件,因此初始“加载”实际上是即时的,但任何尝试矩阵的访问范围将使用虚拟内存来分页文件的正确范围。因此,它消除了任何扫描分隔符并将 IO 推迟到绝对需要。 (如果有任何你永远不会访问的范围?它们永远不会被加载。)

    内存映射的另一大好处是,如果多个进程每个只读内存映射相同的磁盘文件,操作系统就足够智能,可以让它们共享任何公共的分页范围。因此,如果有 3 个完全独立的操作系统进程,每个进程都映射同一个文件,您可以节省 3 倍的 RAM。

    (如果在所有这些更改之后,重新启动服务器进程的延迟仍然是一个问题 - 可能是因为服务器进程崩溃或需要经常重新启动 - 你甚至可以考虑使用一些 other long-lived , 稳定的进程来初始映射向量。然后,即使所有服务器进程崩溃也不会导致操作系统丢失文件的任何分页范围,并且服务器进程的重新启动可能会找到部分或全部相关数据已经在 RAM 中。但是一旦其他优化到位,这个额外角色的复杂性可能是多余的。)

    一个额外的警告:如果您开始使用 KeyedVectors 之类的 .most_similar() 方法,它可以(直到 gensim-3.8.3)触发创建单位长度规范化字向量的全尺寸缓存,您可以除非您采取一些额外的步骤来缩短该过程,否则将失去 mmap 的好处。在之前的答案中查看更多详细信息:How to speed up Gensim Word2vec model load time?

    【讨论】:

    • 非常感谢您提供的非常详细的信息,我认为这是有道理的。
    猜你喜欢
    • 2016-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-19
    • 1970-01-01
    • 1970-01-01
    • 2018-06-26
    • 2019-08-07
    相关资源
    最近更新 更多