Hugging Face发布TurboQuant量化教程,适配Qdrant
向量搜索系统需要为每次查询保留大量的嵌入集合。随着集合的增长,存储和读取float32向量的成本变得高昂。量化使用更少的位数,但可能改变最近邻的排序。
本文通过TurboQuant研究这一权衡。我们将了解它为何旋转向量、其工作原理,以及Qdrant增加了哪些内容。我还对基础float32、二元量化和标量量化与TurboQuant进行了基准测试。
为什么TurboQuant填补了向量压缩的空白
向量搜索将查询嵌入与存储的嵌入进行比较,以找到邻近项。考虑一百万个1,024维向量。Float32每个坐标使用四个字节:
1,000,000 × 1,024 × 4 = 4.096 GB
每个坐标四位时,编码需要约0.512 GB。这8倍的缩减不包括元数据、载荷和HNSW图。向量编码大小为向量数 × 维度 × 位数 / 8。一个1,024维向量在float32下需要4,096字节,TQ4下512字节,TQ2下256字节,TQ1下128字节。这些数字不包括集合的其余部分。
现有方法存在空白。标量量化提供4倍压缩。二元量化可提供32倍压缩,但召回率强烈依赖于嵌入分布。乘积量化通常需要训练和存储码本。
TurboQuant支持四位、两位、一位半和一位模式。当标量量化占用过多内存时,从TQ4开始。当预算接近二元量化但召回率仍然重要时,测试TQ1或TQ2。当向量主导内存或存储时,使用TurboQuant。使用您的嵌入、查询、HNSW设置和重打分策略进行测试。
让我们了解其工作原理……
TurboQuant的工作原理
嵌入值并非总是均匀分布在坐标上。直接将其压缩为少量位数可能丢失有用信息。
TurboQuant首先应用随机正交旋转。这会混合坐标,同时保持向量长度、点积和L2距离。它改变坐标系,而不改变维度数量。
对于归一化向量,该过程有三个主要步骤。
1. 旋转向量以将其值分布到各坐标上。
2. 将每个旋转后的值映射到固定码本中的最近条目。
3. 存储条目的索引,而不是原始浮点值。
四位为每个坐标提供16个可能的索引。Lloyd-Max码本将代表值放置在值更频繁出现的区域更紧密的位置。这减少了平均重构误差。
固定码本来自旋转的已知分布。在论文的随机旋转下,单位向量的每个坐标遵循缩放后的Beta分布。在高维中,这接近高斯分布或钟形分布。因此,码本可以预先计算,而无需在每个数据集上训练。
在搜索期间,查询也会被旋转。然后可以从存储的编码估计相似性,而无需将存储的向量旋转回来。压缩仍然引入误差。论文的MSE变体减少重构误差,而其PROD变体为有偏内积估计添加一位校正。Qdrant使用MSE并添加下述内容。
交互式TurboQuant演练展示了旋转、分布、码本和重构向量。更改维度和位预算以查看它们如何影响重构误差。
Qdrant中的TurboQuant实现
Qdrant 1.18引入了TurboQuant作为可选的量化层。它搜索压缩副本,并可以使用保留的原始向量对候选进行重打分。该实现结合了TurboQuant的固定Lloyd-Max码本与RaBitQ的思想,包括长度校正和一位评分。
TurboQuant演练可视化涵盖了通用码本和Lloyd-Max的数学分解:https://arkaung.github.io/interactive-turboquant/#codebook
Qdrant应用长度重归一化,这是从RaBitQ借鉴的校正,以抵消压缩引入的向量缩短,并校准每个坐标,使数据与固定码本对齐。这些调整共同减少评分误差,但无法完全恢复量化中丢失的信息。
存储校正的相同4字节还携带向量的原始L2范数,在评分时乘回,使点积和欧氏距离成为一等指标,而不仅仅限于余弦相似性。评分随后在整数上运行:码本和查询都被量化,以便单个SIMD指令处理整个坐标块,使一个查询可以与数百万个压缩向量进行比较,每个块仅需少量指令。
阅读Qdrant的文档指南,了解其如何计算长度重归一化、估计坐标调整(逐坐标校准),以及使用SIMD加速对不同位深度进行评分。
在TurboQuant、标量量化和二元量化之间选择
我们使用三个数据集进行基准测试:SciFact、ArguAna和NFCorpus。它们涵盖不同的检索任务:寻找科学主张的证据、识别反驳论点以及检索医学信息。这三个数据集都是BEIR的一部分,BEIR是一个用于评估跨领域信息检索的基准套件。
nDCG分数直接取自数据集:
每个数据集包含文档、查询和相关性判断(qrels)。我们使用来自CohereLabs/beir-embed-english-v3的预计算文档和查询嵌入。这些嵌入使用embed-english-v3.0生成,具有1,024维。使用预计算嵌入消除了嵌入API成本,并确保每种量化方法操作相同的向量。
现在,我们使用相同的文档和测试查询比较六种配置:
- F32将每个坐标存储为32位浮点值,不进行量化。它提供了衡量检索质量变化的参考。
- 标量量化SQ8将浮点坐标映射为8位整数。其向量编码使用float32空间的四分之一,而舍入可能改变相似性分数。
- 二元量化BQ1和BQ2使用一位或两位编码坐标。这提供了更小的向量编码,但检索质量的损失取决于嵌入分布。
- TurboQuant在一位(TQ1)和四位(TQ4)下测试,使用前述旋转和量化过程。
- TQ4每个坐标使用的位数是SQ8的一半。TQ1和BQ1共享相同的标称位预算,这使我们能够在相同编码大小下比较它们的检索质量。
注意:实现中我使用了HNSW配置,m=16和ef_construct=128(类似于Qdrant博客使用的),并使用常见的信息检索指标进行评估:精确率、召回率和nDCG。
每个指标在测试查询中取平均。当数据集每个查询的相关文档很少时,Precision@10可能较低。找到唯一相关文档给出0.1的精确率,即使召回率为1.0。
Qdrant还发布了使用arXiv标题、DBpedia实体、Wikipedia嵌入、H&M产品目录、LAION图像嵌入和广告嵌入的基准测试。其TurboQuant文章中的比较表仅报告召回率,不包含精确率或nDCG。
文章:https://qdrant.tech/articles/turboquant-quantization/
在Qdrant中实现TurboQuant
TurboQuant在Qdrant 1.18中作为quantization_config选项提供,因此从float32、SQ或BQ切换只需更改配置并重新索引。应用程序的其余代码与您一直使用的相同。
pip install "qdrant-client[fastembed]"
连接到Qdrant云
将客户端指向您的云端点,并从FastEmbed加载嵌入模型。将API密钥和端点URL保存在环境变量中。
立即创建您的免费Qdrant集群:https://cloud.qdrant.io/
import os
from qdrant_client import QdrantClient, models
from fastembed import TextEmbedding
client = QdrantClient(
url=os.environ["QDRANT_URL"],
api_key=os.environ["QDRANT_API_KEY"],
)
embedder = TextEmbedding("jinaai/jina-embeddings-v2-base-en")
使用TurboQuant配置创建集合
向量大小必须与您的嵌入模型匹配,量化在集合创建时声明一次。这是流程中与未量化设置唯一不同的部分。
client.create_collection(
collection_name="catalog",
vectors_config=models.VectorParams(
size=768,
distance=models.Distance.COSINE,
),
quantization_config=models.TurboQuantization(
turbo=models.TurboQuantQuantizationConfig(
bits=models.TurboQuantBitSize.BITS4,
),
),
)
bits字段控制编码深度,默认为bits4。可用值为BITS4、BITS2、BITS1_5和BITS1,分别提供8倍、16倍、约21倍和32倍压缩。
插入文档
写入时无需更改。您发送普通的float32向量,Qdrant在索引期间压缩它们,因此无论是否启用量化,相同的摄取代码都有效。
products = [
"waterproof hiking boots with ankle support",
"lightweight running shoes for marathon training",
"insulated stainless steel water bottle, 1 litre",
"merino wool base layer for cold weather",
"collapsible trekking poles with cork grips",
]
vectors = list(embedder.embed(products))
client.upsert(
collection_name="catalog",
points=[
models.PointStruct(
id=i,
vector=v.tolist(),
payload={"text": text}
)
for i, (v, text) in enumerate(zip(vectors, products))
],
)
通过传递查询进行推理并重打分
搜索压缩编码速度快但近似。过采样从量化索引中拉取更宽的候选集,然后重打分使用保留的原始向量对这些候选重新排序,这是召回率差距大部分被弥补的地方。
query = next(embedder.embed(["shoes for walking long distances outdoors"]))
hits = client.query_points(
collection_name="catalog",
query=query.tolist(),
limit=3,
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=True,
oversampling=2.0, # 获取6个候选,返回最佳3个
),
),
).points
for hit in hits:
print(round(hit.score, 4), hit.payload["text"])
输出:
何时使用TurboQuant
如果您使用标量量化,当压缩向量占用过多内存时,测试TQ4。与SQ8相比,它将标称编码大小减半,我们的结果显示检索质量相似。
如果您使用二元量化,在相同位深度下比较TurboQuant。我们的一位结果倾向于TQ1优于BQ1,但在切换前测量延迟和吞吐量。召回率提升可能不值得为您的应用牺牲更慢的搜索。
根据Qdrant的说明:在相同存储预算下尝试TurboQuant(BQ 2位 → TQ 2位,BQ 1.5位 → TQ 1.5位,BQ 1位 → TQ 1位)。在本文描述的基准测试中,它始终提供更高的召回率,通常在16倍和32倍存储类别中提高10–20个百分点。如果您在工作负载中观察到吞吐量明显下降,或召回率提升对您的用例影响太小,则继续使用BQ。
就是这样。我希望您觉得这篇文章有见地。