FastApi 和 SpaCy 的内存错误

MemoryError with FastApi and SpaCy

我是 运行 一个 FastAPI (v0.63.0) web app that uses 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对象是class的一个实例,它的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 似乎也是一个看似合理的根本原因。

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

类似的 discussion about an old SpaCy issue 建议偶尔重新加载 nlp 对象可能是一种解决方法。我不确定这是否仍然适用于更新的 SpaCy 版本,以及应该如何解决。

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

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

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