使用 Flask 和 Gunicorn 在生产中加载预训练手套

Loading pretrained glove on production with flask and Gunicorn

我有一个模型需要使用斯坦福的 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) 的模型的推荐方法是什么

在某种程度上,如果您需要它在内存中,这就是将 gigabyte-plus 从磁盘读取到有用的 RAM 结构所花费的时间,那么是的 - 这就是进程准备就绪所花费的时间使用该数据。但是还有优化的空间!

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

正如您担心的那样,如果 3 个 gunicorn 工作人员每个 运行 相同的加载代码,将加载相同数据的 3 个单独副本——但下面有一种方法可以避免这种情况。

我建议首先将向量加载到实用程序 class 中以访问 word-vectors,例如 Gensim 库中的 KeyedVectors 接口。它将所有向量存储在一个紧凑的 numpy 矩阵中,具有一个 dict-like 接口,每个单独的向量仍然 returns 一个 numpy ndarray

例如,您可以将 GLoVe text-format 向量转换为 slightly-different 交换格式(带有额外的 header 行,Gensim 在使用后调用 word2vec_format原始 Google word2vec.c 代码)。在 gensim-3.8.3(截至 2020 年 8 月的当前版本)中,您可以:

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

然后,utility-class 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)。但是这个 headerless-format 会慢一点,因为它需要遍历文件两次 - 第一次了解完整大小。)

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

不过,您可以采取其他措施,以最大限度地减少 parsing-on-load,并推迟加载不需要的整组矢量范围,并在进程之间自动重用原始数组数据。

那个额外的步骤是 re-save 使用 Gensim 实例的 .save() 函数的向量,这会将原始向量转储到一个单独的密集文件中,该文件适合 memory-mapping 在下一个加载。那么首先:

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

这将创建 多个文件,如果搬迁,这些文件必须保存在一起 – 但保存的 .npy 文件将是准备好的最小格式memory-mapping.

然后,以后需要的时候加载为:

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

mmap 参数使用底层 OS 机制简单地将相关矩阵 address-space 映射到磁盘上的 (read-only) 个文件,以便initial 'load' 实际上是即时的,但任何访问矩阵范围的尝试都将使用 virtual-memory 到 page-in 文件的正确范围。因此,它消除了任何 scanning-for-delimiters 并推迟 IO 直到绝对需要。 (如果有任何范围你永远不会访问?它们永远不会被加载。)

memory-mapping 的另一大好处是,如果多个进程每个 read-only memory-map 相同的 on-disk 文件,OS 足够智能让他们共享任何共同的 paged-in 范围。因此,比方说,3 totally-separate OS 个进程,每个进程映射相同的文件,您将节省 3 倍的 RAM。

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

一个额外的警告:如果你开始使用像 .most_similar() 这样的 KeyedVectors 方法,它可以(直到 gensim-3.8.3)触发 full-size 缓存的创建 unit-length-normalized word-vectors,你可能会失去 mmap 的好处,除非你采取一些额外的步骤来 short-circuit 该过程。在先前的答案中查看更多详细信息:How to speed up Gensim Word2vec model load time?