knn — VPE-Accelerated KNN
knn/main.cpp holds the device-kernel implementation of the same search the XVector C API exposes as a single call, next to the host driver that dispatches it. Read it to see the machinery the C API hides — manual data partitioning, per-task dispatch, and top-k merge.
Each task reads its own slice of the dataset (via mu::getTaskIdx() / mu::getTaskCount()), scores it against the query, and writes its top-k into [taskIdx*k, taskIdx*k + k); the host merges the per-task results. The L2 kernel loads the target once, then for each dataset vector computes L2² = (v − target)·(v − target) with sub + dot, keeping a bounded max-heap of the k smallest distances:
__pxl_kernel__ void knnVpeL2(const float* data, const float* target,
uint32_t vectorCount, uint32_t dim, uint32_t k,
float* distResult, uint32_t* labelResult)
{
uint32_t taskIdx = mu::getTaskIdx();
uint32_t taskCount = mu::getTaskCount();
uint32_t perTask = vectorCount / taskCount;
uint32_t start = taskIdx * perTask;
uint32_t end = (taskIdx + 1 == taskCount) ? vectorCount : start + perTask; // last task absorbs remainder
mu::vdma::VpeContext ctx(mu::vdma::VpeIdStrategy::ByThreadId);
uint64_t bufs[3];
// false only if the request could never fit an empty pool -- with a
// runtime dimension that is worth checking; the output array is untouched.
if (!ctx.waitAllocateBuffers(3, ctx.bytesForDimension(dim), bufs))
return;
uint64_t queryBuf = bufs[0], targetBuf = bufs[1], diffBuf = bufs[2];
ctx.load(target, targetBuf, dim); // load the query target once, reuse across the slice
std::vector<std::pair<float, uint32_t>> topk; // bounded max-heap: root = current worst
topk.reserve(k);
for (uint32_t v = start; v < end; ++v)
{
ctx.load(data + v * dim, queryBuf, dim);
ctx.sub(queryBuf, targetBuf, diffBuf, dim);
float dist = ctx.dot(diffBuf, diffBuf, dim);
if (topk.size() < k) { topk.emplace_back(dist, v); std::push_heap(topk.begin(), topk.end()); }
else if (dist < topk.front().first)
{
std::pop_heap(topk.begin(), topk.end());
topk.back() = {dist, v};
std::push_heap(topk.begin(), topk.end());
}
}
std::sort_heap(topk.begin(), topk.end()); // ascending: smallest distance first
// write topk -> distResult/labelResult at [taskIdx*k, ...)
}
A companion knnVpeDot kernel does the same with inner-product similarity — no diff buffer, and a min-heap keeping the k largest scores. The host partitions the dataset so the smallest task still has ≥ k points, dispatches both kernels, merges the per-task top-k, and verifies against a CPU reference.
./run.sh knn # run with defaults
./run.sh knn -- --dim 128 --vectors 4096 --k 10 # forward args to the example
# or, from the build/ directory:
./bin/xarith_knn --dim 128 --vectors 4096 --k 10
Options:
| Option | Default | Description |
|---|---|---|
--dim N | 128 | Vector dimension |
--vectors N | 960000 | Number of dataset vectors |
--k N | 5 | Number of nearest neighbors |
Environment Variables:
| Variable | Default | Description |
|---|---|---|
DEVICEID | 0 | Device ID |
NUMSUB | (device) | Number of subsystems. Unset asks the device, which differs per part |
TASKCOUNT | 48 | Number of subtasks for data partitioning (must be ≥ NUMSUB) |