速度提高100倍 - 扩展 RAG 应用程序,以实现数十亿个嵌入,并行计算余弦相似度

原文链接:100x Faster — Scaling Your RAG App for Billions of Embeddings

2024 年 2 月 15 日

RAG应用程序最大的问题之一是它们的计算检索时间。想象一下,你有一个向量数据库,包含一万亿条Embedding向量的记录。当您尝试将用户查询与一万亿向量匹配时,检索正确的信息肯定要花费一分钟以上的时间。

我们能否在CPU内核上使用并行处理将检索正确信息的时间缩短到几秒钟?

减少时间包括找到有效的方法来计算用户查询Embedding向量与存储在向量数据库中的百万,十亿,甚至万亿个其他Embedding向量之间的余弦相似度。

Chunkdot,在MIT许可下,是专门为此目的而设计的,为密集矩阵和稀疏矩阵提供多线程矩阵乘法。它适用于对项目矩阵表示进行分段(Embeddings),并使用Numba加速计算,从而计算出大量项目中K个最相似的项目。

HuggingFace上有很多数据集,提供了超过100万个条目的Embedding向量,比如这个来自Qdrant的dataset。你可以用它来测试Chunkdot的性能。然而,对于详细的性能测量,我们将使用NumPy库来生成各种维度的随机Embedding向量。

我们将比较两种方法,一种来自Chunkdot,另一种是余弦相似度的伪代码。我们将观察增加大小和维度对性能的影响。我将使用Kaggle(无GPU)笔记本来完成这项任务,以确保一致性。

这个博客的所有代码都可以在我的GitHub存储库中找到。

目录表

  • 舞台设置

  • 编码伪码算法

  • 编码块点算法

  • 编码计算时间函数

  • 测试10k向量Embeddings

  • 测试100k向量Embeddings

  • 测试100万个向量Embeddings

  • 可视化可扩展性影响

  • Chunkdot功能

  • 下一步是什么

搭建舞台

Chunkdot需要与其他库类似的安装过程。

1
2
# installing chunkdot
pip install chunkdot

在运行任何东西之前,我们必须首先检查Kaggle环境中的可用内存。

1
2
# Checking available memory
!free -h

img

可用内存在Kaggle笔记本

检查可用内存对Chunkdot至关重要。随着向量数据库大小的增加,计算内存也会增加。为了防止超出可用内存,监控硬件中的剩余内存非常重要。在我的情况下,可用空间是25GB,不包括Buff/Cache。

让我们导入必要的库。

1
2
3
4
5
6
7
8
# to matrix generate matrices
import numpy as np

# importing cosine similarity module from chunkdot
from chunkdot import cosine_similarity_top_k

# to calculate computation time
import timeit

伪代码算法

我们将首先构建一个伪代码算法,计算用户查询向量与其他数百万个可能存储在数据库或本地的向量之间的余弦相似度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def cosine_pseudocode(query_v, doc_v, num_indices):
    """
    Retrieve indices of the highest cosine similarity values between
    the query vector and embeddings.
    
    Parameters:
        query_v (numpy.ndarray): Query vector.
        doc_v (list of numpy.ndarray): List of embedding vectors.
        num_indices (int): Number of Top indices to retrieve.
        
    Returns:
        list of int: Indices of the highest cosine similarity values.
    """
    cosine_similarities = []  # Initialize an empty list to store cosine similarities

    query_norm = np.linalg.norm(query_v)  # Calculate the norm of the query vector
    
    # Iterate over each documents embedding vectors in the list
    for vec in doc_v:
        dot_product = np.dot(vec, query_v.T)  # Calculate dot product between embedding vector and query vector
        embedding_norm = np.linalg.norm(vec)  # Calculate the norm of the embedding vector
        cosine_similarity = dot_product / (embedding_norm * query_norm)  # Calculate cosine similarity
        cosine_similarities.append(cosine_similarity)  # Append cosine similarity to the list
    
    cosine_similarities = np.array(cosine_similarities)  # Convert the list to a numpy array
    
    # Sort the array in descending order
    sorted_array = sorted(range(len(cosine_similarities)), key=lambda i: cosine_similarities[i], reverse=True)

    # Get indices of the top two values
    top_indices = sorted_array[:num_indices]
    
    # Return the indices of highest cosine similarity values
    return top_indices

这个余弦相似度函数,独立于除NumPy以外的任何库,接受三个输入:

  • query_v用户查询的Embedding向量
  • doc_v存储在某处的文档的Embedding向量
  • num_indices类似top_k结果的文档索引号

Chunkdot算法

现在我们已经编写了伪代码算法,下一步是编写Chunkdot余弦相似度函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def cosine_chunkdot(query_v, doc_v, num_indices, max_memory):
    """
    Calculate cosine similarity using the chunkdot library.
    
    Parameters:
        query_v (numpy.ndarray): Query vector.
        doc_v (numpy.ndarray): List of Embedding vectors.
        num_indices (int): Number of top indices to retrieve.
        max_memory (float): Maximum memory to use.
        
    Returns:
        numpy.ndarray: Top k indices.
    """
    
    # Calculate Cosine Similarity
    cosine_array = cosine_similarity_top_k(embeddings=query_v, embeddings_right=doc_v, 
                                          top_k=num_indices, max_memory=max_memory)  # Calculate cosine similarity using chunkdot

    # Get indices of the top values
    top_indices = cosine_array.nonzero()[1]
    
    # return the top similar results
    return top_indices

这个Chunkdot函数有四个输入:

  • query_v用户查询的Embedding向量
  • doc_v存储在某处的文档的Embedding向量
  • num_indices类似top_k结果的文档索引号
  • max_memory表示计算的可用内存,其值以字节为单位。例如,1E9表示1GB, 10E9表示10GB,以此类推。

让我们在一个样本数据集上测试这两个函数,观察它们的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
doc_embeddings = np.random.randn(10, 100) # 10 document embeddings (100 dim)

user_query = np.random.rand(1,100) # 1 user query (100 dim)

top_indices = 1 # number of top indices to retrieve

max_memory = 5E9 # maximum memory to use (5GB)

# retrieve indices of the highest cosine similarity values using pseudocode
print("top indices using pseudocode:", cosine_pseudocode(user_query, doc_embeddings, top_indices))

# retrieve indices of the highest cosine similarity values using chunkdot
print("top indices using chunkdot:", cosine_chunkdot(user_query, doc_embeddings, top_indices, max_memory))
### OUTPUT ###
top indices using pseudocode: [4]
top indices using chunkdot: [4]
### OUTPUT ###

我为文档Embeddings生成了10个随机Embedding向量,每个向量的维度为100,还有一个用户查询,它是具有相同维度的单个Embedding向量。’ top_indices ‘参数设置为1,这意味着它将根据最高余弦相似度返回文档Embeddings中仅一个相似项的索引。内存使用率设置为5E9,等于5GB。我们的两个函数都返回相同的索引4,这表明我们对两个函数都进行了准确的编码。

编码计算时间函数

我们还需要创建一个计时函数,它可以测量这两个函数输出结果所花费的计算时间。

1
2
3
4
5
6
7
8
9
10
11
12
# calculate time taken
def calculate_execution_time(query_v, doc_v, num_indices, max_memory, times):
    
    # calculate time taken to execute the pseudocode function
    pseudocode_time = round(timeit.timeit(lambda: cosine_pseudocode(query_v, doc_v, num_indices), number=times), 5)

    # calculate time taken to execute the chunkdot function
    chunkdot_time = round(timeit.timeit(lambda: cosine_chunkdot(query_v, doc_v, num_indices, max_memory), number=times), 5)

    # print the time taken
    print("Time taken for pseudocode function:", pseudocode_time, "seconds")
    print("Time taken for chunkdot function:", chunkdot_time, "seconds")

我们已经回顾了传递给这个函数的参数。这里唯一的新参数是times,它告诉函数你想运行代码多少次。让我们在更大的规模上测试Chunkdot性能的效率。

测试10k向量Embeddings

我们将从合理数量的文档Embeddings开始,10000个,这相当于一个小规模的特定于领域的RAG应用程序。我将每个Embedding向量的维度设置为1536,这相当于OpenAIEmbedding模型text-embedding-3-small

让我们通过运行100次来计算每种方法的计算时间。

1
2
3
4
5
6
7
8
9
10
doc_embeddings = np.random.randn(10000, 1536) # 10K document embeddings (1536 dim)

user_query = np.random.rand(1,1536) # user query (1536 dim)

top_indices = 1 # number of top indices to retrieve 

max_memory = 5E9 # maximum memory set to 5GB

# compute the time taken to execute the functions
calculate_execution_time(user_query, doc_embeddings, top_indices, max_memory, 100)

对于10k个文档Embeddings,维度为1536,两种算法运行100次,对比如下:

img

10k文档计算时间

与我们的伪代码相比,Chunkdot需要更多的时间。这是因为它首先创建块,并在合并它们之前对每个块执行计算。因此,对于这个小规模的例子,它可能不是一个合适的解决方案。但是,当我们稍后使用更大的示例时,您将看到Chunkdot的好处。

测试100k向量Embeddings

对于10K,我们的伪代码方法获胜,但是现在让我们将文档Embedding向量增加到100K向量,这与中等规模的RAG应用程序相当。

让我们计算每种方法的计算时间,但这次我们将“times”参数设置为1(只运行一次代码),因为向量的数量相当大,并且不需要多次执行计算。

1
2
3
4
5
6
7
8
9
10
11
12
doc_embeddings = np.random.randn(100000, 1536) # 100K document embeddings (1536 dim)

user_query = np.random.rand(1,1536) # user query (1536 dim)

top_indices = 1 # number of top indices to retrieve 

max_memory = 5E9 # maximum memory set to 5GB

times = 1 # number of times to execute the functions

# compute the time taken to execute the functions
calculate_execution_time(user_query, doc_embeddings, top_indices, max_memory, times)

对于100k的文档Embeddings,维度为1536,运行两种算法一次,下面是比较:

img

100k文档计算时间

与我们的伪代码相比,Chunkdot花费的时间更少,几乎是一半。现在我们看到了Chunkdot带来的积极影响。

一百万向量Embeddings的测试

处理涉及数百万个Embeddings的任务时,您需要检查的第一件事是文档Embedding向量占用了多少内存。

1
2
3
4
5
6
7
8
9
# 1 Million document embeddings (1536 dim)
doc_embeddings = np.random.randn(1000000, 1536)

# user query (1536 dim)
user_query = np.random.rand(1,1536)

# Check the memory size of doc_embeddings and user_query embedding
print(doc_embeddings.nbytes / (1024 * 1024 * 1024),
      user_query.nbytes / (1024 * 1024))

img

100万个Embedding向量的内存大小

我们的文档Embeddings大约占用12GB。让我们检查一下剩余的可用空间。

img

检查可用空间

我们有高达17GB的可用内存。为了避免任何内存错误,我们将为max_memory参数设置一个安全值,即12GB。让我们看看结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1 Million document embeddings (1536 dim)
doc_embeddings = np.random.randn(1000000, 1536)

# user query (1536 dim)
user_query = np.random.rand(1,1536)

top_indices = 1 # number of top indices to retrieve 

max_memory = 12E9 # maximum memory set to  --- 12GB ---

times = 1 # number of times to execute the functions

# compute the time taken to execute the functions
calculate_execution_time(user_query, doc_embeddings, top_indices, max_memory, times)

img

100万个文档计算时间

ChunkDot确实有效地减少了计算量。当你打算构建一个严肃的RAG应用程序时,你应该考虑从至少一百万个查询开始。工作与更高维度的Embedding模型,高达4000。这种方法将变得更加有效。

可视化可伸缩性影响

让我们可视化增加文档Embedding向量数量的影响,从10,000开始到一个非常大的数字。

img

不同数量文档的计算时间

我绘制了三种方法,在增加文档Embeddings数量的基础上,Chunkdot是所有方法中最优越的。现在,让我们看看Embedding向量的维数是如何影响计算时间的。

img

不同维度的计算时间

我在增加向量维度的同时使用了100K个文档,在增加文档数量时观察到的行为与我们看到的相同。

Chunkdot的特点

Chunkdot有一个可以显示进度条的功能,它可以帮助您跟踪剩余的计算量。

1
2
3
4
5
6
7
8
9
10
11
12
doc_embeddings = np.random.randn(100000, 1536) # 100K document embeddings (1536 dim)

user_query = np.random.rand(1,1536) # user query (1536 dim)

top_indices = 100 # number of top indices to retrieve 

max_memory = 5E9 # maximum memory set to 5GB

# with progress bar
output_array = cosine_similarity_top_k(user_query, doc_embeddings, 
                        top_k=top_indices, 
                        show_progress=True)

img

进度条示例

Chunkdot的输出是一个稀疏矩阵,您可以使用以下命令将其转换为数组:

1
2
# converting the ouput
output_array.toarray()

您可以仅对文档Embeddings使用Chunkdot,它将为文档Embeddings的每个元素返回top_k个最相似的元素。

1
2
3
4
5
6
7
8
9
10
11
12
# total 5 documents embeddings
embeddings = np.random.randn(5, 256)

# return top 2 most similar item index for each
cosine_similarity_top_k(embeddings, top_k=2).toarray()
### OUTPUT ###
array([[1.        , 0.        , 0.        , 0.        , 0.09924064],
       [0.        , 1.        , 0.        , 0.09935381, 0.        ],
       [0.02358785, 0.        , 1.        , 0.        , 0.        ],
       [0.        , 0.09935381, 0.        , 1.        , 0.        ],
       [0.09924064, 0.        , 0.        , 0.        , 1.        ]])
### OUTPUT ###

类似地,您可以通过向top_k参数提供负值来返回最不相似的项

1
2
3
4
5
6
7
8
9
10
11
12
13
# total 5 documents embeddings
embeddings = np.random.randn(5, 256)

# return top 2 most dissimilar item index for each 
# Top_K = -2
cosine_similarity_top_k(embeddings, top_k=-2).toarray()
### OUTPUT ###
array([[ 0.        ,  0.        , -0.04357524,  0.        , -0.05118288],
       [ 0.        ,  0.        ,  0.        ,  0.01619543, -0.01836534],
       [-0.04357524,  0.        ,  0.        , -0.02466613,  0.        ],
       [ 0.        ,  0.01619543, -0.02466613,  0.        ,  0.        ],
       [-0.05118288, -0.01836534,  0.        ,  0.        ,  0.        ]])
### OUTPUT ###

这可能不是你的情况,但如果你处理的是10K维的稀疏Embeddings,你可以使用“密度”参数来更有效地减少计算。

1
2
3
4
5
6
7
8
9
# for creating sparse embeddings
from scipy import sparse

# creating spare matrix with 100K documents (10K dim each)
# defining density of 0.005
embeddings = sparse.rand(100000, 10000, density=0.005)

# using all you system's memory
cosine_similarity_top_k(embeddings, top_k=50)

接下来

如果您想了解Chunkdot算法是如何工作的,请查看作者的这个有意思的博客。Chunkdot最大的好处之一是它可以在CPU内核上工作。未来,他们计划集成GPU支持,这将大大减少计算时间。如果你的本地环境没有足够的RAM,你可以使用像Kaggle或GitHub Codespaces这样的平台,与GPU成本相比,云CPU内核和RAM的成本非常低。不要忘记查看官方GitHub存储库和他们的博客,因为它非常好地解释了Chunkdot是如何工作的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/408084.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python中操作MySQL和SQL Server数据库的基础与实战【第97篇—MySQL数据库】

Python中操作MySQL和SQL Server数据库的基础与实战 在Python中,我们经常需要与各种数据库进行交互,其中MySQL和SQL Server是两个常见的选择。本文将介绍如何使用pymysql和pymssql库进行基本的数据库操作,并通过实际代码示例来展示这些操作。…

eclipse中open Type 、 open type in Hierachy、open Resource的区别

目录 场景: open Type open Resource open type in Hierachy 场景: 在项目中想要研究底层代码,经常要用eclipse看依赖jar包的类,比如spring的源码中AbstractApplicationContext类CTLSHIFTT用的少,经常用的CTLSHIR…

微信小程序 uniapp+vue餐厅美食就餐推荐系统

本论文根据系统的开发流程以及一般论文的结构分为三个部分,第一个部分为摘要、外文翻译、目录;第二个部分为正文;第三个部分为致谢和参考文献。其中正文部分包括: (1)绪论,对课题背景、意义、目…

[rust] 10 project, crate, mod, pub, use: 项目目录层级组织, 概念和实战

文章目录 一 项目目录层级组织概念1.1 cargo new 创建同名 的 Project 和 crate1.2 多 crate 的 package1.3 mod 模块1.3.1 创建嵌套 mod1.3.2 mod 树1.3.3 用路径引用 mod1.3.3.1 使用绝对还是相对? 1.3.4 代码可见性1.3.4.1 pub 关键字1.3.4.2 用 super 引用 mod1.3.4.3 用 …

docker安装flink

docker安装flink 5.1、拉取flink镜像,创建网络 docker pull flink docker network create flink-network5.2、创建 jobmanager # 创建 JobManager docker run \-itd \--namejobmanager \--publish 8081:8081 \--network flink-network \--env FLINK_PROPERTIES&…

【全网首发】上周申请的谷歌Gemini 1.5 Pro已通过!百万token的Gemini 1.5 Pro开箱测试(一)

大家好,我是木易,一个持续关注AI领域的互联网技术产品经理,国内Top2本科,美国Top10 CS研究生,MBA。我坚信AI是普通人变强的“外挂”,所以创建了“AI信息Gap”这个公众号,专注于分享AI全维度知识…

C# WPF 桌面应用程序使用 SQlite 数据库

我们在开发 WPF 桌面应用程序时,数据库存的使用是必不可少的,除非你的应用没有数据存储的需求,有了数据存储需求,我们就会面临使用什么样的数据库的选择问题,我的选择方案是,单机版的应用我优先选择 Sqlite…

Mac安装Appium

一、环境依赖 一、JDK环境二、Android-SDK环境(android自动化)三、Homebrew环境四、Nodejs 安装cnpm 五、安装appium六、安装appium-doctor来确认安装环境是否完成七、安装相关依赖 二、重头大戏, 配置wda(WebDriverAgent&#x…

小苯的IDE括号问题(CD) -----牛客小白月赛87(双链表)

C题&#xff1a;C-小苯的IDE括号问题&#xff08;easy&#xff09;_牛客小白月赛87 (nowcoder.com) D题&#xff1a; D-小苯的IDE括号问题&#xff08;hard&#xff09;_牛客小白月赛87 (nowcoder.com) C题代码&#xff1a; #include<bits/stdc.h>using namespace std…

番外篇 | YOLOv5+DeepSort实现行人目标跟踪检测

前言:Hello大家好,我是小哥谈。DeepSort是一种用于目标跟踪的深度学习算法。它结合了目标检测和目标跟踪的技术,能够在视频中准确地跟踪多个目标,并为每个目标分配一个唯一的ID。DeepSort的核心思想是将目标检测和目标跟踪两个任务进行联合训练,以提高跟踪的准确性和稳定性…

Sentinel微服务流量治理组件实战上

目录 分布式系统遇到的问题 解决方案 Sentinel 是什么&#xff1f; Sentinel 工作原理 Sentinel 功能和设计理念 流量控制 熔断降级 Sentinel工作主流程 Sentinel快速开始 Sentinel资源保护的方式 基于API实现 SentinelResource注解实现 Spring Cloud Alibaba整合…

一键生成PDF即刻呈现:轻松创建无忧体验

在信息爆炸的时代&#xff0c;我们每天都在与各种文件、资料打交道。无论是工作中的报告、合同&#xff0c;还是学习中的笔记、论文&#xff0c;如何高效、安全地管理这些珍贵的资料&#xff0c;成为了我们迫切的需求。幸运的是&#xff0c;随着科技的发展&#xff0c;我们不再…

Mysql--索引分类

Mysql--索引分类 1. 索引分类2. 聚集索引&二级索引 1. 索引分类 在MySQL数据库&#xff0c;将索引的具体类型主要分为以下几类&#xff1a;主键索引、唯一索引、常规索引、全文索引。 2. 聚集索引&二级索引 而在在InnoDB存储引擎中&#xff0c;根据索引的存储形式&am…

Mysql运维篇(五) 部署MHA--主机环境配置

一路走来&#xff0c;所有遇到的人&#xff0c;帮助过我的、伤害过我的都是朋友&#xff0c;没有一个是敌人。如有侵权&#xff0c;请留言&#xff0c;我及时删除&#xff01; 大佬博文 https://www.cnblogs.com/gomysql/p/3675429.html MySQL 高可用&#xff08;MHA&#x…

【算法与数据结构】417、LeetCode太平洋大西洋水流问题

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;题目要求雨水既能流向太平洋也能流向大西洋的网格。雨水流向取决于网格的高度。一个比较直接的方式是对…

LangChain Agent v0.2.0简明教程 (中)

4. Retrieval4.1 Document loaders4.2 Text Splitter4.3 Text embedding models4.4 Vector stores4.5 Retrievers4.6 Indexing4. Retrieval 许多LLM需要使用特定用户的外部数据,这些数据不属于模型训练集的一部分。实现这一目标的主要方法是通过检索增强生成(RAG)。在此过程…

window: C++ 获取自己写的dll的地址

我自己用C写了一个插件,插件是dll形式的,我的插件式在dll的目录下有个config文件夹,里面是我用json写的插件配置文件,当插件运行的时候我需要读取到json配置文件,所有最重要的就是如何获取dll的路径. 大概就是这么个结构, 我自己封装了一个函数.只适用于window编程,因为里面用…

【ArcGIS】利用DEM进行水文分析:流向/流量等

利用DEM进行水文分析 ArcGIS实例参考 水文分析通过建立地表水文模型&#xff0c;研究与地表水流相关的各种自然现象&#xff0c;在城市和区域规划、农业及森林、交通道路等许多领域具有广泛的应用。 ArcGIS实例 某流域30m分辨率DEM如下&#xff1a; &#xff08;1&#xff09…

【大数据】Flink 内存管理(一):设置 Flink 进程内存

Flink 内存管理&#xff08;一&#xff09;&#xff1a;设置 Flink 进程内存 1.配置 Total Memory2.JVM 参数3.根据比例限制的组件&#xff08;Capped Fractionated Components&#xff09; Apache Flink 通过严格控制各种组件的内存使用&#xff0c;在 JVM 上提供高效的工作负…

LabVIEW储氢材料循环寿命测试系统

LabVIEW储氢材料循环寿命测试系统 随着氢能技术的发展&#xff0c;固态储氢技术因其高密度和安全性成为研究热点。储氢材料的循环寿命是衡量其工程应用的关键。然而&#xff0c;传统的循环寿命测试设备存在成本高、测试效率低、数据处理复杂等问题。设计了一种基于LabVIEW软件…