Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ require (
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/twpayne/go-geom v1.6.1
github.com/viterin/vek v0.4.3
github.com/xdg/scram v1.0.5
go.etcd.io/etcd/raft/v3 v3.5.29
go.opencensus.io v0.24.0
Expand Down Expand Up @@ -88,7 +87,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chewxy/math32 v1.11.1 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand Down Expand Up @@ -157,7 +155,6 @@ require (
github.com/spf13/afero v1.15.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/viterin/partial v1.1.0 // indirect
github.com/xdg/stringprep v1.0.3 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
Expand Down
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8=
github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
Expand Down Expand Up @@ -524,10 +522,6 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
github.com/viterin/partial v1.1.0 h1:iH1l1xqBlapXsYzADS1dcbizg3iQUKTU1rbwkHv/80E=
github.com/viterin/partial v1.1.0/go.mod h1:oKGAo7/wylWkJTLrWX8n+f4aDPtQMQ6VG4dd2qur5QA=
github.com/viterin/vek v0.4.3 h1:cogdlNjd6EJYtNbmTN0lJCey2htrfSo1AHWpc6DVncQ=
github.com/viterin/vek v0.4.3/go.mod h1:A4JRAe8OvbhdzBL5ofzjBS0J29FyUrf95tQogvtHHUc=
github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw=
github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4=
Expand Down
19 changes: 11 additions & 8 deletions tok/hnsw/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
c "github.com/dgraph-io/dgraph/v25/tok/constraints"
"github.com/dgraph-io/dgraph/v25/tok/index"
"github.com/pkg/errors"
"github.com/viterin/vek"
"github.com/viterin/vek/vek32"
)

const (
Expand Down Expand Up @@ -96,20 +94,25 @@ func applyDistanceFunction[T c.Float](a, b []T, floatBits int, funcName string,
// function, hence it takes in a floatBits parameter,
// but doesn't actually use it.
func dotProduct[T c.Float](a, b []T, floatBits int) (T, error) {
return applyDistanceFunction(a, b, floatBits, "dot product", vek32.Dot, vek.Dot)
return applyDistanceFunction(a, b, floatBits, "dot product", dotF32, dotF64)
}

// This needs to implement signature of SimilarityType[T].distanceScore
// function, hence it takes in a floatBits parameter.
func cosineSimilarity[T c.Float](a, b []T, floatBits int) (T, error) {
return applyDistanceFunction(a, b, floatBits, "cosine distance", vek32.CosineSimilarity, vek.CosineSimilarity)
return applyDistanceFunction(a, b, floatBits, "cosine distance", cosineSimF32, cosineSimF64)
}

// euclideanDistance returns the metric-domain euclidean distance, i.e. square rooted.
// Callers rely on that: DistanceThreshold in SearchWithOptions compares against it
// directly. The square root is monotonic, so dropping it would leave ranking unchanged
// but would require squaring the threshold at those comparison sites.
//
// This needs to implement signature of SimilarityType[T].distanceScore
// function, hence it takes in a floatBits parameter,
// but doesn't actually use it.
func euclideanDistanceSq[T c.Float](a, b []T, floatBits int) (T, error) {
return applyDistanceFunction(a, b, floatBits, "euclidean distance", vek32.Distance, vek.Distance)
func euclideanDistance[T c.Float](a, b []T, floatBits int) (T, error) {
return applyDistanceFunction(a, b, floatBits, "euclidean distance", euclideanF32, euclideanF64)
}

// Used for distance, since shorter distance is better
Expand Down Expand Up @@ -217,7 +220,7 @@ type SimilarityType[T c.Float] struct {
func GetSimType[T c.Float](indexType string, floatBits int) SimilarityType[T] {
switch {
case indexType == Euclidean:
return SimilarityType[T]{indexType: Euclidean, distanceScore: euclideanDistanceSq[T],
return SimilarityType[T]{indexType: Euclidean, distanceScore: euclideanDistance[T],
insortHeap: insortPersistentHeapAscending[T], isBetterScore: isBetterScoreForDistance[T],
isSimilarityMetric: false}
case indexType == Cosine:
Expand All @@ -229,7 +232,7 @@ func GetSimType[T c.Float](indexType string, floatBits int) SimilarityType[T] {
insortHeap: insortPersistentHeapDescending[T], isBetterScore: isBetterScoreForSimilarity[T],
isSimilarityMetric: true}
default:
return SimilarityType[T]{indexType: Euclidean, distanceScore: euclideanDistanceSq[T],
return SimilarityType[T]{indexType: Euclidean, distanceScore: euclideanDistance[T],
insortHeap: insortPersistentHeapAscending[T], isBetterScore: isBetterScoreForDistance[T],
isSimilarityMetric: false}
}
Expand Down
42 changes: 42 additions & 0 deletions tok/hnsw/kernels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package hnsw

// This file documents the distance-kernel contract shared by the two build-tagged
// implementations in kernels_simd.go (//go:build goexperiment.simd) and
// kernels_generic.go (//go:build !goexperiment.simd).
//
// Each implementation provides, for both float32 and float64:
//
// dotF32/dotF64 - sum(a[i]*b[i])
// euclideanSqF32/euclideanSqF64 - sum((a[i]-b[i])^2), NOT square-rooted
// euclideanF32/euclideanF64 - sqrt of the above, the metric-domain distance
// cosineSimF32/cosineSimF64 - dot(a,b) / sqrt(dot(a,a)*dot(b,b))
//
// Contract for every kernel:
//
// - Callers guarantee len(a) == len(b), and applyDistanceFunction enforces it by
// returning an error before any kernel is reached. Each kernel then reslices b to
// len(a) so the compiler can eliminate bounds checks on b inside the loop. Note
// that this reslice is not itself a length check: b[:len(a)] succeeds whenever
// cap(b) >= len(a), so passing a short subslice of a longer array reads the
// elements beyond its length rather than panicking. The guard is the wrapper, not
// the kernel.
// - Zero-length input yields 0 for dot and euclidean, and NaN for cosine (0/0).
// This matches the behaviour of a zero vector and is strictly safer than the
// vek implementation this replaced, which panicked on empty input.
// - Results are not bit-identical to a naive left-to-right summation. Both
// implementations use multiple independent accumulators, and the SIMD path
// additionally uses fused multiply-add, so partial sums are reassociated and
// rounded differently. Relative error against a float64 reference stays within
// a few ULP of float32 (~4e-7 measured at 768 dimensions), which is far below
// the resolution at which ranking decisions differ. Tests must compare with a
// tolerance rather than for exact equality.
//
// Why multiple accumulators: the obvious `sum += a[i]*b[i]` loop is bound by the
// latency of the floating-point add dependency chain, not by throughput. Splitting
// into independent partial sums lets the CPU keep several adds in flight, which is
// worth roughly 2x on its own before any vectorisation.
165 changes: 165 additions & 0 deletions tok/hnsw/kernels_generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//go:build !goexperiment.simd

/*
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package hnsw

import "math"

// Portable distance kernels used when the build does not enable GOEXPERIMENT=simd.
// See kernels.go for the contract these must satisfy.
//
// These are deliberately unrolled into independent accumulators rather than written
// as the obvious single-accumulator loop. Dot product and squared euclidean use a
// 4-way unroll; cosine already has three independent chains (dot, |a|, |b|) so it
// uses a 2-way unroll, giving six, which is enough to saturate the FP pipeline
// without risking register spills.

func dotF32(a, b []float32) float32 {
b = b[:len(a)]
var s0, s1, s2, s3 float32
i := 0
for ; i+4 <= len(a); i += 4 {
// The loads are hoisted into locals rather than indexed inline in the
// accumulation. That is not cosmetic: under go1.27.0 on arm64 the inline form
// generates a 1.6x slower loop (253ns vs 158ns at 768 dimensions), while the
// hoisted form matches go1.26.5. The other two kernels here happen to hoist
// already, via their difference and product temporaries.
a0, b0 := a[i], b[i]
a1, b1 := a[i+1], b[i+1]
a2, b2 := a[i+2], b[i+2]
a3, b3 := a[i+3], b[i+3]
s0 += a0 * b0
s1 += a1 * b1
s2 += a2 * b2
s3 += a3 * b3
}
for ; i < len(a); i++ {
s0 += a[i] * b[i]
}
return (s0 + s1) + (s2 + s3)
}

func dotF64(a, b []float64) float64 {
b = b[:len(a)]
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(a); i += 4 {
// The loads are hoisted into locals rather than indexed inline in the
// accumulation. That is not cosmetic: under go1.27.0 on arm64 the inline form
// generates a 1.6x slower loop (253ns vs 158ns at 768 dimensions), while the
// hoisted form matches go1.26.5. The other two kernels here happen to hoist
// already, via their difference and product temporaries.
a0, b0 := a[i], b[i]
a1, b1 := a[i+1], b[i+1]
a2, b2 := a[i+2], b[i+2]
a3, b3 := a[i+3], b[i+3]
s0 += a0 * b0
s1 += a1 * b1
s2 += a2 * b2
s3 += a3 * b3
}
for ; i < len(a); i++ {
s0 += a[i] * b[i]
}
return (s0 + s1) + (s2 + s3)
}

func euclideanSqF32(a, b []float32) float32 {
b = b[:len(a)]
var s0, s1, s2, s3 float32
i := 0
for ; i+4 <= len(a); i += 4 {
d0 := a[i] - b[i]
d1 := a[i+1] - b[i+1]
d2 := a[i+2] - b[i+2]
d3 := a[i+3] - b[i+3]
s0 += d0 * d0
s1 += d1 * d1
s2 += d2 * d2
s3 += d3 * d3
}
for ; i < len(a); i++ {
d := a[i] - b[i]
s0 += d * d
}
return (s0 + s1) + (s2 + s3)
}

func euclideanSqF64(a, b []float64) float64 {
b = b[:len(a)]
var s0, s1, s2, s3 float64
i := 0
for ; i+4 <= len(a); i += 4 {
d0 := a[i] - b[i]
d1 := a[i+1] - b[i+1]
d2 := a[i+2] - b[i+2]
d3 := a[i+3] - b[i+3]
s0 += d0 * d0
s1 += d1 * d1
s2 += d2 * d2
s3 += d3 * d3
}
for ; i < len(a); i++ {
d := a[i] - b[i]
s0 += d * d
}
return (s0 + s1) + (s2 + s3)
}

func euclideanF32(a, b []float32) float32 {
return float32(math.Sqrt(float64(euclideanSqF32(a, b))))
}

func euclideanF64(a, b []float64) float64 {
return math.Sqrt(euclideanSqF64(a, b))
}

func cosineSimF32(a, b []float32) float32 {
b = b[:len(a)]
var d0, d1, x0, x1, y0, y1 float32
i := 0
for ; i+2 <= len(a); i += 2 {
av0, bv0 := a[i], b[i]
av1, bv1 := a[i+1], b[i+1]
d0 += av0 * bv0
d1 += av1 * bv1
x0 += av0 * av0
x1 += av1 * av1
y0 += bv0 * bv0
y1 += bv1 * bv1
}
for ; i < len(a); i++ {
d0 += a[i] * b[i]
x0 += a[i] * a[i]
y0 += b[i] * b[i]
}
dot, na, nb := d0+d1, x0+x1, y0+y1
return dot / float32(math.Sqrt(float64(na)*float64(nb)))
}

func cosineSimF64(a, b []float64) float64 {
b = b[:len(a)]
var d0, d1, x0, x1, y0, y1 float64
i := 0
for ; i+2 <= len(a); i += 2 {
av0, bv0 := a[i], b[i]
av1, bv1 := a[i+1], b[i+1]
d0 += av0 * bv0
d1 += av1 * bv1
x0 += av0 * av0
x1 += av1 * av1
y0 += bv0 * bv0
y1 += bv1 * bv1
}
for ; i < len(a); i++ {
d0 += a[i] * b[i]
x0 += a[i] * a[i]
y0 += b[i] * b[i]
}
dot, na, nb := d0+d1, x0+x1, y0+y1
return dot / math.Sqrt(na*nb)
}
Loading
Loading