QBit реорганизует хранение векторов, чтобы ускорить приближенный поиск. Вместо того чтобы хранить элементы каждого вектора вместе, он группирует одинаковые позиции битов во всех векторах.
При этом векторы хранятся с полной точностью, а во время поиска вы можете выбирать уровень квантования: считывать меньше битов, чтобы сократить I/O и ускорить вычисления, или больше битов для более высокой точности. Это дает выигрыш в скорости за счет уменьшения объема передаваемых данных и вычислений благодаря квантованию, при этом все исходные данные при необходимости остаются доступными.
Чтобы объявить столбец типа QBit, используйте следующий синтаксис:
element_type– тип каждого элемента вектора. Допустимые типы:Int8,BFloat16,Float32иFloat64dimension– размерность, то есть число элементов в каждом вектореstride– необязательный параметр. Число размерностей, хранящихся вместе в одной группе потоков. Если не указано, по умолчанию используетсяdimension(одна группа). Если указано,dimensionдолжно быть кратноstride, а еслиstrideменьшеdimension, тоstrideдолжно быть кратно 8. Размерностиdimensionразбиваются наdimension / strideпоследовательных групп, а битовые плоскости каждой группы хранятся в отдельных потоках. Это позволяет при поиске по первымDразмерностям (гдеDкратноstride) считывать только потоки тех групп, которые охватывают эти размерности, что полезно для эмбеддингов Matryoshka.
Создание QBit
QBit при определении столбца таблицы:
Преобразование массивов в QBit
QBit, если длина массива соответствует размерности QBit. Тип элементов массива не обязательно должен совпадать с типом элементов QBit. Любой числовой тип элементов автоматически преобразуется в нужный тип. Это позволяет напрямую перенести существующий столбец эмбеддингов в столбец QBit:
CAST, например CAST(embedding AS QBit(Float32, 8)).
Преобразование QBit в массивы
QBit к Array возвращает сохранённые значения. Это обратная операция к преобразованию массивов в QBit:
QBit, после чего его элементы преобразуются в запрошенный тип элементов массива. Поэтому работает и приведение, которое одновременно меняет тип элементов, например из QBit(Float32, N) в Array(Float64).
Преобразование по цепочке Array -> QBit -> Array не приводит к потере данных для Int8, Float32 и Float64. Для BFloat16 оно соответствует прямому преобразованию в BFloat16 — единственная потеря точности связана с самим BFloat16.
Когда dimension не кратна 8, конечные элементы дополнения, присутствующие во внутреннем представлении, отбрасываются, поэтому результат всегда содержит ровно dimension элементов.
Преобразование между типами QBit
QBit можно привести к другому QBit, если размерность (число элементов вектора) остается прежней. При этом и element_type, и stride могут изменяться; приведение к QBit с другой размерностью вызывает исключение, поскольку это привело бы к изменению самого вектора.
Изменение element_type заново строит вектор и преобразует каждый элемент в новый тип, в точности как при соответствующем преобразовании Array: расширение (например, из QBit(Float32, N) в QBit(Float64, N)) происходит без потери точности, тогда как сужение приводит к потере точности так же, как и сужающее приведение Array.
stride (при сохранении того же element_type) перегруппировывает сохранённые битовые плоскости, не затрагивая значения, поэтому потерь данных не возникает:
Подстолбцы QBit
QBit реализует шаблон доступа к подстолбцам, который позволяет обращаться к отдельным битовым плоскостям сохранённых векторов. Доступ к каждой битовой позиции можно получить с помощью синтаксиса .N, где N — это позиция бита:
Int8: 8 подстолбцов на группу stride (1-8)BFloat16: 16 подстолбцов на группу stride (1-16)Float32: 32 подстолбца на группу stride (1-32)Float64: 64 подстолбца на группу stride (1-64)
vec.N считывает битовую плоскость (N-1) % element_size группы stride (N-1) / element_size. Например, в QBit(BFloat16, 4096, 1024) 4096 размерностей разбиваются на 4 группы по 1024, поэтому всего получается 64 подстолбца: vec.1 … vec.16 — это битовые плоскости первой группы stride (размерности 1–1024), vec.17 … vec.32 относятся ко второй группе (размерности 1025–2048), и так далее.
Страйды
QBit хранит каждую битовую плоскость как один поток, охватывающий все dimension размерностей, поэтому при поиске всегда считываются целые битовые плоскости по всему вектору. Необязательный параметр stride разбивает dimension размерностей на dimension / stride последовательных групп и хранит битовые плоскости каждой группы в отдельных потоках. Это позволяет при поиске только по первым D размерностям (где D кратно stride) считывать только потоки тех групп, которые покрывают эти размерности, — это полезно для эмбеддингов Matryoshka, где начальные размерности образуют пригодный для использования эмбеддинг меньшей размерности.
BFloat16 (16 битовых плоскостей) vec.1 … vec.16 — это 16 битовых плоскостей первой группы stride (размерности 1–1024), vec.17 … vec.32 относятся ко второй группе (размерности 1025–2048) и так далее. В общем случае vec.N считывает битовую плоскость (N-1) % element_size группы stride (N-1) / element_size.
Чтобы выполнить поиск с уменьшенной размерностью, передайте число считываемых размерностей в качестве четвёртого аргумента транспонированных функций расстояния (см. ниже). Опорный вектор должен содержать как минимум столько элементов (любые дополнительные конечные элементы игнорируются), а это значение должно быть кратно stride.
Доступ к элементам и срезы
arrayElement (и оператор vec[n]) возвращает n-й элемент вектора, восстановленный с полной точностью типа элемента. Считываются только битовые плоскости группы stride, содержащей этот элемент. arraySlice возвращает QBit по выбранным размерностям (проекцию на подмножество размерностей); смещение и длина должны быть константами, поскольку размерность QBit является частью его типа. Срез, выровненный по границам групп stride, сохраняет stride и повторно использует сохранённые потоки битовых плоскостей без копирования:
Функции векторного поиска
QBit:
Для QBit со stride эти функции принимают необязательный четвёртый аргумент used_dims — число начальных размерностей для чтения, — при этом читаются только группы stride, охватывающие эти размерности. Опорный вектор должен содержать как минимум used_dims элементов (любые дополнительные элементы в конце игнорируются, поэтому вектор запроса полной размерности можно повторно использовать для поиска в уменьшенной размерности, не обрезая его заранее), а used_dims должен быть кратен stride.