首页 > 解决方案 > FastApi 和 SpaCy 的 MemoryError

问题描述

我正在运行一个FastAPI (v0.63.0) Web 应用程序,它使用SpaCy (v3.0.5) 对输入文本进行标记。Web 服务运行一段时间后,总内存使用量增长过大,SpaCy 抛出 MemoryErrors,导致 Web 服务出现 500 错误。

XXX.internal web[17552]: MemoryError:
XXX.internal web[17552]: INFO:     xxx - "POST /page HTTP/1.1" 500 Internal Server Error
XXX.internal web[17552]: ERROR:    Exception in ASGI application
[...]
XXX.internal web[17552]: Traceback (most recent call last):
XXX.internal web[17552]: File "spacy/tokens/token.pyx", line 263, in spacy.tokens.token.Token.text.__get__                                                                                                                                        
XXX.internal web[17552]: File "spacy/tokens/token.pyx", line 806, in spacy.tokens.token.Token.orth_.__get__                                                                                                                                       
XXX.internal web[17552]: File "spacy/strings.pyx", line 132, in spacy.strings.StringStore.__getitem__
XXX.internal web[17552]: KeyError: "[E018] Can't retrieve string for hash '10429668501569482890'. This usually refers to an issue with the `Vocab` or `StringStore`." 

这是我的相关部分main.py

@app.post(f"/page", response_model=PageResponse)
async def classify(request: PageRequest):
    try:
        preprocessed = await preprocessor.preprocess(request.text)
    [...]

preprocessor对象是一个类的实例,它的preprocess方法调用 SpaCy 标记器:

class SpacyTokenizer(Tokenizer):
    def __init__(self, nlp: spacy.Language):
        self._nlp = spacy.load("en_core_web_sm")

        for component in self._nlp.pipe_names:
            # we only need tokenization
            self._nlp.remove_pipe(component)

    def tokenize(self, text: str) -> Iterable[str]:
        if len(text) >= self._nlp.max_length:
            raise ValueError(f"Text too long: {len(text)} characters.")

        try:
            doc = self._nlp(text)
            return islice(
                (token.text for token in doc), settings.SPACY_MAX_TOKENS
            )
        except MemoryError:
            raise ValueError(f"Text too long: {len(text)} characters.")

正如您在代码中看到的那样,我试图通过限制生成的令牌数量并捕获MemoryError. 不过,两者似乎都没有任何效果(我确实知道从MemoryError概念上捕获通常不会起作用)。

我观察到服务器机器上的工作进程随着时间的推移不断使用更多内存:

17552 webapp    20   0 2173336   1,6g   7984 S  4,7 79,9  33:29.04 uvicorn                                                                                                                                                                                                        

当该过程开始时,该uvicorn过程需要〜700MB而不是1,6g。

从错误消息中,我想很明显 SpaCy 标记器是罪魁祸首。但是,我希望它在处理请求时释放一个工作线程以释放其内存,因此 FastAPI 或 Uvicorn 似乎也是一个合理的根本原因。

我的主要问题是:我在哪里以及如何调试它?

关于旧 SpaCy 问题的类似讨论表明,nlp偶尔重新加载对象可能是一种解决方法。我不确定这是否仍然适用于更新的 SpaCy 版本,以及应该如何解决。

另一方面,是否有 FastAPI 或 Uvicorn 选项可以处理释放其线程的内存?

标签: nlpspacyfastapiuvicorn

解决方案


SpaCy 标记器似乎在内部将每个标记缓存在地图中。因此,每个新标记都会增加该映射的大小。随着时间的推移,不可避免地会出现越来越多的新代币(尽管速度会降低,遵循 Zipf 定律)。在某些时候,在处理了大量文本之后,令牌映射将因此超出可用内存。有了大量可用内存,这当然可以延迟很长时间。

我选择的解决方案是将 SpaCy 模型存储在 TTLCache 中每小时重新加载一次,清空令牌映射。这为重新加载 SpaCy 模型增加了一些额外的计算成本,但这几乎可以忽略不计。


推荐阅读