使用烧瓶和Gunicorn在生产中加载经过预训练的手套

问题描述

我有一个模型,需要使用Stanford的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实例都需要将同一副手套装载到内存中?这意味着服务器资源将非常大,请让我知道这里是否正确。

最重要的是,我的建议是在生产服务器上托管需要预先训练的嵌入(手套/ word2vec / fasttext)的模型的推荐方法是什么

解决方法

在一个级别上,如果您需要它在内存中,那就是从磁盘读取千兆字节以上的数据到有用的RAM结构所花费的时间,那么是的-这是一个进程准备使用该数据之前所花费的时间。 。但是还有优化的空间!

例如,将其作为Pandas数据帧的第一个读取,然后将其转换为Python dict,比其他选项需要更多的步骤和更多的RAM。 (在瞬间glove_pdglove都已完全构造和引用的情况下,您将在内存中拥有两个完整副本,而且两个副本都不如理想的那么紧凑,这可能会导致其他性能下降,特别是如果膨胀完全触发使用任何虚拟内存的情况。)

您可能会担心,如果3个gunicorn工人各自运行相同的加载代码,则将加载3个相同数据的单独副本–但是下面有一种避免这种情况的方法。

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

例如,您可以将GLoVe文本格式矢量转换为稍有不同的交换格式(带有额外的标题行,Gensim在原始Google word2vec_format代码使用后会调用word2vec.c) 。在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)。但是此无头文件-format会稍慢一些,因为它需要两次遍历文件-第一次是要学习完整大小。)

与您原来的通用两步过程相比,将一次加载到KeyedVectors中应该已经更快,更紧凑。并且,与您在先前dict上执行的操作类似的查找将在glove_kv实例上可用。 (此外,还有许多其他便捷操作,例如排名.most_similar()查找,它们利用有效的数组库功能来提高速度。)

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

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

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

这将创建多个文件,如果重定位,则必须将它们保存在一起-但是保存的.npy文件将是准备用于内存映射的确切最小格式。 / p>

然后,在以后需要时加载为:

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

mmap参数使用底层OS机制将相关的矩阵地址空间简单地映射到磁盘上的(只读)文件,因此初始的“加载”实际上是即时的,但是任何尝试访问矩阵的范围将使用虚拟内存在文件的正确范围内进行分页。因此,它消除了任何扫描定界符和延迟IO的需求,直到绝对需要为止。 (并且,如果有任何范围您从未访问过?它们将永远不会被加载。)

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

(如果经过所有这些更改,仍然是重新启动服务器进程的滞后问题-也许是因为服务器进程崩溃或需要经常重新启动-您甚至可以考虑使用一些长期存在的 other 一个稳定的进程,以最初映射向量,然后,即使所有服务器进程崩溃也不会导致操作系统丢失文件的所有分页范围,并且服务器进程的重启可能会发现一些或所有相关的数据已经存在于RAM中。但是,一旦其他优化措施到位,这种额外作用的麻烦可能就变得多余了。

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