dify文本嵌入慢问题排查记录

缘起

在做一个dify的演示项目的时候,需要用到rag技术,解析上传的文档然后交给llm用于更好的回答用户的问题

至于什么是rag,怎样学习rag,这里推荐一个非常好的入门项目:
https://github.com/datawhalechina/all-in-rag

问题

在下图可以看到,我上传了一个1.5MB的文件到dify中,但是嵌入居然执行了2.84个小时,

在这里插入图片描述

环境

我使用的本地ollama运行的bge-m3-q4_k_m
本地显卡是RTX 4050 笔记本版 显存6g(有点穷)

在执行嵌入的时候,cpu 5%以下, 显卡负载几乎为零.
推测大概率是软件配置上的问题…

尝试1

网络上给ollama增加ollama_num_parallel这个参数试一试,
增加完重启ollama看到已经识别了这个变量.
在这里插入图片描述
测试发现,嵌入速度基本没变化,cpu,gpu占用率都没上去,后面在探究原因

尝试2

有人说bge模型有点大2G网上,那么换个小点的几百兆的
我找到了一个小模型,名字叫Dmeta-embedding-zh
https://modelscope.cn/models/VBNn0x/Dmeta-embedding-zh-gguf/files
我使用了q5-km的量化版本,整个模型下载下来才50M
导入ollama试一下
在这里插入图片描述

性能应该是有提升的:
起码现在python的cpu占用率最高了,观察进度确实快了很多,但是cpu和gpu还是没有占用到满

可能是由于我ollama设置的进程数是16,所以可以适当缩小进程数看看性能是不是好一点.
在这里插入图片描述
等这个文档索引完看看用了多长时间.
实测,时间并没变短.

测试

我使用http的方式调用了一下embeding
发现使用bge需要2s,换成ggml则只需要27ms.
那么感觉嵌入速度慢的问题可能卡在了调度上,不然怎么基本没变化.
网络上基本没有找到dify关于文本嵌入调度方面的资料,看来这部分要自己读代码了.

根据代码排查

上方两个方法都用了之后不见生效,只能通过阅读源码的方式进行排查,最终发下如下两个卡点.

卡点一

看到嵌入过程的一段处理逻辑,这段代码会不断地生成JiebaKeywordTableHandler对象,但是问题是生成这个对象是个正常重的操作.请看下段代码

    def create(self, texts: list[Document], **kwargs) -> BaseKeyword:
        lock_name = "keyword_indexing_lock_{}".format(self.dataset.id)
        with redis_client.lock(lock_name, timeout=600):
            keyword_table_handler = JiebaKeywordTableHandler()
            keyword_table = self._get_dataset_keyword_table()
            for text in texts:
                keywords = keyword_table_handler.extract_keywords(
                    text.page_content, self._config.max_keywords_per_chunk
                )
                if text.metadata is not None:
                    self._update_segment_keywords(self.dataset.id, text.metadata["doc_id"], list(keywords))
                    keyword_table = self._add_text_to_keyword_table(
                        keyword_table or {}, text.metadata["doc_id"], list(keywords)
                    )

            self._save_dataset_keyword_table(keyword_table)

            return self

在做嵌入的时候,需要用到jieba分词,但是这个对象会调用很多次,每次调用都需要在初始化函数里面使用import jieba.analyse
然而,这个初始化操作是非常重的操作,jieba的加载会初始化词典.并且这个过程还是一个全局锁进行,导致大量的进程都在等待锁

import re
from typing import Optional, cast


class JiebaKeywordTableHandler:
    def __init__(self):
        import jieba.analyse  # type: ignore

        from core.rag.datasource.keyword.jieba.stopwords import STOPWORDS

        jieba.analyse.default_tfidf.stop_words = STOPWORDS  # type: ignore

    def extract_keywords(self, text: str, max_keywords_per_chunk: Optional[int] = 10) -> set[str]:
        """Extract keywords with JIEBA tfidf."""
        import jieba.analyse  # type: ignore

        keywords = jieba.analyse.extract_tags(
            sentence=text,
            topK=max_keywords_per_chunk,
        )
        # jieba.analyse.extract_tags returns list[Any] when withFlag is False by default.
        keywords = cast(list[str], keywords)

        return set(self._expand_tokens_with_subtokens(set(keywords)))

    def _expand_tokens_with_subtokens(self, tokens: set[str]) -> set[str]:
        """Get subtokens from a list of tokens., filtering for stopwords."""
        from core.rag.datasource.keyword.jieba.stopwords import STOPWORDS

        results = set()
        for token in tokens:
            results.add(token)
            sub_tokens = re.findall(r"\w+", token)
            if len(sub_tokens) > 1:
                results.update({w for w in sub_tokens if w not in list(STOPWORDS)})

        return results

从火焰图中可以看到,这个init函数占据了36%的时间,他调用的find_and_load是个带锁的函数,大量的时间都白白的浪费在了这里.因为
在这里插入图片描述
为什么要把import放在init里面呢?
原始代码将 import jieba.analyse 放在 init 方法内部,作者可能出于以下几种考虑:

  1. 延迟加载(Lazy Loading) :

    • 开发者可能希望只在实际需要使用jieba库时才加载它,而不是在模块导入时就加载
    • 这可以减少模块初始加载时间,特别是当这个类可能不总是被使用的情况下
  2. 避免不必要的依赖 :

    • 如果jieba是一个可选依赖,或者只在特定场景下需要使用
    • 放在方法内部可以避免在不需要使用该功能时加载不必要的依赖
  3. 解决循环导入问题 :

    • 在某些复杂的项目结构中,将导入放在方法内部可以避免潜在的循环导入问题
  4. 动态配置需求 :

    • 可能开发者认为在实例化时才加载可以根据不同的实例配置不同的jieba设置
  5. 避免全局状态污染 :

    • 希望每个实例都有自己独立的jieba环境,避免全局修改影响其他模块

卡点二

再看火焰图右半边,这里面同样有一个模块加载引起的性能损失,占9%的执行时间
在这里插入图片描述
再看代码:同样是import引入的祸.两个执行,两个卡点总共占用了45%时间,都是在import工具,等锁
在这里插入图片描述

结论

在ollama中更换了较小的嵌入模型依旧很慢的原因是dify在处理文件的时候多次重复的加载jieba分词的框架,导致所有线程都进行了阻塞.但是具体如何解决本人还没有考虑好,有处理方法欢迎大家在评论区补充

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值