* -------------------------------------------------------------------------
* This file is part of the IndexSDK project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* IndexSDK is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
#include "MathUtils.h"
namespace math_utils {
float CalcDistance(float *vec1, float *vec2, size_t dim)
{
float dist = 0;
for (size_t j = 0; j < dim; j++) {
dist += (vec1[j] - vec2[j]) * (vec1[j] - vec2[j]);
}
return dist;
}
void ComputeVecsL2sq(float *vecsL2sq, float *data, const size_t numPoints, const size_t dim)
{
#pragma omp parallel for schedule(static, 8192)
for (int64_t nIter = 0; nIter < static_cast<int64_t>(numPoints); nIter++) {
vecsL2sq[nIter] = cblas_snrm2(static_cast<blasint>(dim), (data + (nIter * dim)), 1);
vecsL2sq[nIter] *= vecsL2sq[nIter];
}
}
void ComputeClosestCentersInBlock(const float *const data, const size_t numPoints, const size_t dim,
const float *const centers, const size_t numCenters,
const float *const docsL2sq, const float *const centersL2sq,
uint32_t *centerIndex, float *const distMatrix, size_t k)
{
DISK_THROW_IF_NOT_MSG(k <= numCenters, "k and numCenters should be equal.");
auto onesCenters = std::make_unique<float[]>(numCenters);
auto onesPoints = std::make_unique<float[]>(numPoints);
for (size_t i = 0; i < numCenters; i++) {
onesCenters[i] = 1.0;
}
for (size_t i = 0; i < numPoints; i++) {
onesPoints[i] = 1.0;
}
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, static_cast<blasint>(numPoints),
static_cast<blasint>(numCenters), static_cast<blasint>(1), 1.0f,
docsL2sq, static_cast<blasint>(1), onesCenters.get(), static_cast<blasint>(1), 0.0f,
distMatrix, static_cast<blasint>(numCenters));
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, static_cast<blasint>(numPoints),
static_cast<blasint>(numCenters), static_cast<blasint>(1), 1.0f,
onesPoints.get(), static_cast<blasint>(1), centersL2sq, static_cast<blasint>(1), 1.0f,
distMatrix, static_cast<blasint>(numCenters));
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, static_cast<blasint>(numPoints),
static_cast<blasint>(numCenters), static_cast<blasint>(dim), -2.0f,
data, static_cast<blasint>(dim), centers, static_cast<blasint>(dim), 1.0f,
distMatrix, static_cast<blasint>(numCenters));
if (k == 1) {
#pragma omp parallel for schedule(static, 8192)
for (int64_t i = 0; i < static_cast<int64_t>(numPoints); i++) {
float min = std::numeric_limits<float>::max();
float *current = distMatrix + (i * numCenters);
for (size_t j = 0; j < numCenters; j++) {
if (current[j] < min) {
centerIndex[i] = static_cast<uint32_t>(j);
min = current[j];
}
}
}
} else {
#pragma omp parallel for schedule(static, 8192)
for (int64_t i = 0; i < static_cast<int64_t>(numPoints); i++) {
std::priority_queue<PivotContainer> topKQueue;
float *current = distMatrix + (i * numCenters);
for (size_t j = 0; j < numCenters; j++) {
PivotContainer thisPiv(j, current[j]);
topKQueue.push(thisPiv);
}
for (size_t j = 0; j < k; j++) {
PivotContainer thisPiv = topKQueue.top();
centerIndex[i * k + j] = static_cast<uint32_t>(thisPiv.pivID);
topKQueue.pop();
}
}
}
}
void ComputeClosestCenters(float *data, size_t numPoints, size_t dim, float *pivotData, size_t numCenters,
size_t k, uint32_t *closestCentersIvf, std::vector<size_t> *invertedIndex,
float *ptsNormsSquared)
{
DISK_THROW_IF_NOT_MSG(k <= numCenters, "k and numCenters should be equal.");
bool isNormGivenForPts = (ptsNormsSquared != nullptr);
auto pivsNormsSquared = std::make_unique<float[]>(numCenters);
std::unique_ptr<float[]> ptsNormsSquaredSharedPtr;
if (!isNormGivenForPts) {
ptsNormsSquaredSharedPtr = std::make_unique<float[]>(numPoints);
ptsNormsSquared = ptsNormsSquaredSharedPtr.get();
}
size_t parBlockSize = numPoints;
DISK_THROW_IF_NOT(numPoints > 0 && parBlockSize > 0);
size_t nBlocks =
(numPoints % parBlockSize) == 0 ? (numPoints / parBlockSize) : (numPoints / parBlockSize) + 1;
if (!isNormGivenForPts) {
math_utils::ComputeVecsL2sq(ptsNormsSquared, data, numPoints, dim);
}
math_utils::ComputeVecsL2sq(pivsNormsSquared.get(), pivotData, numCenters, dim);
auto closestCenters = std::make_unique<uint32_t[]>(parBlockSize * k);
auto distanceMatrix = std::make_unique<float[]>(numCenters * parBlockSize);
for (size_t curBlk = 0; curBlk < nBlocks; curBlk++) {
float *dataCurBlk = data + curBlk * parBlockSize * dim;
size_t numPtsBlk = std::min(parBlockSize, numPoints - curBlk * parBlockSize);
float *ptsNormsBlk = ptsNormsSquared + curBlk * parBlockSize;
math_utils::ComputeClosestCentersInBlock(dataCurBlk, numPtsBlk, dim, pivotData, numCenters,
ptsNormsBlk, pivsNormsSquared.get(), closestCenters.get(),
distanceMatrix.get(), k);
int64_t totalPointsToProcess = std::min(static_cast<int64_t>(numPoints),
static_cast<int64_t>((curBlk + 1) * parBlockSize));
#pragma omp parallel for schedule(static, 1)
for (int64_t j = curBlk * parBlockSize; j < totalPointsToProcess; j++) {
for (size_t l = 0; l < k; l++) {
size_t thisCenterID = closestCenters[(j - curBlk * parBlockSize) * k + l];
closestCentersIvf[j * k + l] = static_cast<uint32_t>(thisCenterID);
if (invertedIndex != nullptr) {
#pragma omp critical
invertedIndex[thisCenterID].push_back(j);
}
}
}
}
}
}
namespace kmeans {
float LloydsIter(float *data, size_t numPoints, size_t dim, float *centers, size_t numCenters, float *docsL2sq,
std::vector<size_t> *closestDocs, uint32_t *&closestCenter)
{
const size_t MAX_POINTS = 1000000000;
DISK_THROW_IF_NOT(numPoints > 0 && numPoints <= MAX_POINTS);
for (size_t c = 0; c < numCenters; ++c) {
closestDocs[c].clear();
}
math_utils::ComputeClosestCenters(data, numPoints, dim, centers, numCenters, 1, closestCenter, closestDocs,
docsL2sq);
std::fill(centers, centers + numCenters * dim, 0.0f);
#pragma omp parallel for schedule(static, 1)
for (size_t c = 0; c < numCenters; ++c) {
float *center = centers + c * dim;
std::vector<double> clusterSum(dim, 0.0);
for (size_t i = 0; i < closestDocs[c].size(); i++) {
float *current = data + (closestDocs[c][i] * dim);
for (size_t j = 0; j < dim; j++) {
clusterSum[j] += static_cast<double>(current[j]);
}
}
if (closestDocs[c].size() > 0) {
for (size_t i = 0; i < dim; i++) {
center[i] = static_cast<float>(clusterSum[i] / static_cast<double>(closestDocs[c].size()));
}
}
}
float residual = 0.0;
size_t bufPad = 32;
size_t chunkSize = 2 * 8192;
size_t nchunks = numPoints / chunkSize + (numPoints % chunkSize == 0 ? 0 : 1);
std::vector<float> residuals(nchunks * bufPad, 0.0);
#pragma omp parallel for schedule(static, 32)
for (size_t chunk = 0; chunk < nchunks; ++chunk) {
for (size_t d = chunk * chunkSize; d < numPoints && d < (chunk + 1) * chunkSize; ++d) {
residuals[chunk * bufPad] += math_utils::CalcDistance(data + (d * dim),
centers + static_cast<size_t>(closestCenter[d]) * dim,
dim);
}
}
for (size_t chunk = 0; chunk < nchunks; ++chunk) {
residual += residuals[chunk * bufPad];
}
return residual;
}
float RunLloyds(float *data, size_t numPoints, size_t dim, float *centers, const size_t numCenters,
const size_t maxReps, std::vector<size_t> *closestDocs, uint32_t *closestCenter)
{
float residual = std::numeric_limits<float>::max();
std::unique_ptr<std::vector<size_t>[]> closestDocsSmartPtr;
if (closestDocs == nullptr) {
closestDocsSmartPtr = std::make_unique<std::vector<size_t>[]>(numCenters);
closestDocs = closestDocsSmartPtr.get();
}
std::unique_ptr<uint32_t[]> closestCenterSmartPtr;
if (closestCenter == nullptr) {
closestCenterSmartPtr = std::make_unique<uint32_t[]>(numPoints);
closestCenter = closestCenterSmartPtr.get();
}
auto docsL2sq = std::make_unique<float[]>(numPoints);
math_utils::ComputeVecsL2sq(docsL2sq.get(), data, numPoints, dim);
float oldResidual = 0;
for (size_t i = 0; i < maxReps; ++i) {
oldResidual = residual;
residual = LloydsIter(data, numPoints, dim, centers, numCenters, docsL2sq.get(), closestDocs, closestCenter);
if ((i != 0 && ((oldResidual - residual) / residual) < 0.00001) ||
(residual < std::numeric_limits<float>::epsilon())) {
break;
}
}
return residual;
}
void SelectingPivots(float *data, size_t numPoints, size_t dim, float *pivotData, size_t numCenters)
{
DISK_THROW_IF_NOT_MSG(numCenters <= numPoints, "PQ centers should be <= numPoints.");
std::vector<size_t> picked;
std::random_device rd;
auto x = rd();
std::mt19937 generator(x);
std::uniform_int_distribution<size_t> distribution(0, numPoints - 1);
size_t tmpPivot;
for (size_t j = 0; j < numCenters; j++) {
tmpPivot = distribution(generator);
if (std::find(picked.begin(), picked.end(), tmpPivot) != picked.end()) {
continue;
}
picked.push_back(tmpPivot);
std::copy(data + tmpPivot * dim, data + tmpPivot * dim + dim, pivotData + j * dim);
}
}
void KmeansppSelectingPivots(float *data, size_t numPoints, size_t dim, float *pivotData, size_t numCenters)
{
if (numPoints == 0) {
return;
}
if (numPoints > 1 << 23) {
SelectingPivots(data, numPoints, dim, pivotData, numCenters);
return;
}
std::vector<size_t> picked;
std::random_device rd;
auto x = rd();
std::mt19937 generator(x);
std::uniform_real_distribution<> distribution(0, 1);
std::uniform_int_distribution<size_t> intDist(0, numPoints - 1);
size_t initID = intDist(generator);
size_t numPicked = 1;
picked.push_back(initID);
std::copy(data + initID * dim, data + initID * dim + dim, pivotData);
auto dist = std::make_unique<float[]>(numPoints);
#pragma omp parallel for schedule(static, 8192)
for (int64_t i = 0; i < static_cast<int64_t>(numPoints); i++) {
dist[i] = math_utils::CalcDistance(data + i * dim, data + initID * dim, dim);
}
double dartVal;
size_t tmpPivot;
bool sumFlag = false;
while (numPicked < numCenters) {
dartVal = distribution(generator);
double sum = 0;
for (size_t i = 0; i < numPoints; i++) {
sum = sum + dist[i];
}
if (diskann_pro::FloatEqual(sum, 0)) {
sumFlag = true;
}
dartVal *= sum;
double prefixSum = 0;
for (size_t i = 0; i < numPoints; i++) {
tmpPivot = i;
if (dartVal >= prefixSum && dartVal < prefixSum + dist[i]) {
break;
}
prefixSum += dist[i];
}
if (std::find(picked.begin(), picked.end(), tmpPivot) != picked.end() && (sumFlag == false)) {
continue;
}
picked.push_back(tmpPivot);
std::copy(data + tmpPivot * dim, data + tmpPivot * dim + dim, pivotData + numPicked * dim);
#pragma omp parallel for schedule(static, 8192)
for (int64_t i = 0; i < static_cast<int64_t>(numPoints); i++) {
dist[i] = (std::min)(dist[i], math_utils::CalcDistance(data + i * dim, data + tmpPivot * dim, dim));
}
numPicked++;
}
}
}