package require sqlite3 set DEFAULT(--nthread) 16 set DEFAULT(--max-train) 100000 set DEFAULT(--train-function) vec1_train set DEFAULT(--index) {nbucket 1024 codesize 32 opq 1} set DEFAULT(--query) "" set DEFAULT(--meta-data) 0 set DEFAULT(--svd-verify) 0 set DEFAULT(--report) "" set DEFAULT(--repeat) 1 proc usage {} { global DEFAULT puts stderr "usage: $::argv0 ?OPTIONS? DATABASE ?SHLIB1...?" puts stderr "options:" foreach o [array names DEFAULT] { set display $DEFAULT($o) if { [string is integer $display]==0 } { set display "\"$display\"" } puts stderr " [format {% -20s} $o](default: $display)" } exit 1 } proc process_args {lArg} { global C global DEFAULT array set C [array get DEFAULT] for {set i 0} {$i < [expr [llength $lArg]-1]} {incr i} { set a [lindex $lArg $i] if {[string range $a 0 0]!="-"} break if {[string range $a 1 1]!="-"} { set a "-$a" } unset -nocomplain m foreach o [array names C] { if {[string match ${a}* $o]} { if {[info exists m]} usage set m $o } } if {[info exists m]==0} usage incr i set C($m) [lindex $lArg $i] } lrange $lArg $i end } # Process command line arguments and open the database. And load the # vec1.so dynamic extension(s). # set lExt [process_args $argv] if {[llength $lExt]==0} usage set C(dbname) [lindex $lExt 0] set C(lExt) [lrange $lExt 1 end] proc open_db {fname} { global C set lExt $C(lExt); sqlite3 db $fname db enable_load_extension 1 if {[llength $lExt]==0 && [catch {db eval {SELECT vec1_info()}}]} { set shlib [file join [file dirname [info nameofexecutable]] vec1] db eval { SELECT load_extension($shlib) } } else { foreach shlib $lExt { db eval { SELECT load_extension($shlib) } } } db enable_load_extension 0 } proc main { } { global C if {$C(dbname)=="release"} { if {[catch release_test msg]} { puts stderr "ERROR: $msg" } return } open_db $C(dbname) check_db_looks_ok summarize_db build_nearest_if_required set r $C(--repeat) set lResult [list] for {set i 0} {$i < $r} {incr i} { if {$C(--index) != ""} { test_build_index $C(--index) } if {$C(--query) != ""} { lappend lResult [test_query_index {*}$C(--query)] } } if {$C(--query) != ""} { if {$r==1} { print_query_timings } else { puts "recall: [lsort -integer $lResult]" } exit } array set I $C(--index) if {[info exists I(codesize)] && $I(codesize)==0} { # If the command-line specified a code-size of 0, then do not bother # varying K, as reranking won't help anyway. foreach nprobe {16 32 48 64} { test_query_index 1 1 $nprobe test_query_index 10 10 $nprobe } } else { foreach nprobe {16 32 48 64} { test_query_index 1 1 $nprobe test_query_index 10 1 $nprobe test_query_index 50 1 $nprobe test_query_index 100 1 $nprobe test_query_index 200 1 $nprobe test_query_index 300 1 $nprobe test_query_index 10 10 $nprobe test_query_index 50 10 $nprobe test_query_index 100 10 $nprobe test_query_index 200 10 $nprobe test_query_index 300 10 $nprobe test_query_index 100 100 $nprobe test_query_index 200 100 $nprobe test_query_index 400 100 $nprobe test_query_index 600 100 $nprobe test_query_index 800 100 $nprobe test_query_index 1000 100 $nprobe } } print_report } proc release_test_one {varname name index lConfig} { global T upvar $varname lReport test_build_index $index foreach {K recall nprobe} $lConfig { test_query_index $K $recall $nprobe } set times [format_us $T(train)]/[format_us $T(build)] lappend lReport "$name {$index}: train/build: $times" foreach {K recall nprobe} $lConfig { set r $T(recall,$recall,$nprobe,$K) set q $T(qps,$recall,$nprobe,$K) set res "[format {% -6s} $nprobe/$K] -> ${r}@$recall ${q}qps " lappend lReport " $res" } } proc release_test {} { global C set C(--nthread) 8 set lReq [list \ sift1m.db \ landmark-dino-768-cosine.db \ agnews-mxbai-1024-euclidean.db \ ] set nMinByte [expr 1024*1024] # Check that all required databases are present in cwd foreach f $lReq { if {[file exists $f]==0 || [file size $f]<$nMinByte} { error "missing required db file \"$f\"" } } open_db sift1m.db build_nearest_if_required release_test_one lRep SIFT1M "nbucket 1024 codesize 16 opq 0" { 1 1 16 100 1 16 1 1 48 100 1 48 } release_test_one lRep SIFT1M "nbucket 0 codesize 16 opq 0" { 1 1 16 100 1 16 } release_test_one lRep SIFT1M "nbucket 1024 codesize 0 opq 0" { 1 1 16 1 1 48 } open_db landmark-dino-768-cosine.db build_nearest_if_required release_test_one lRep LANDMARK "nbucket 1024 codesize 48 opq 1" { 1 1 16 100 1 48 } open_db agnews-mxbai-1024-euclidean.db build_nearest_if_required release_test_one lRep AGNEWS "nbucket 1024 codesize 32 opq 1" { 1 1 16 100 1 48 } foreach ln $lRep { puts $ln } set fd [open vec1.release.txt a+] puts $fd "--------------------------------------------------------------" foreach ln $lRep { puts $fd $ln } close $fd } proc format_cycles {c} { if {$c>(1000*1000*1000)} { set ret "[format %.2f [expr $c/1000000000.0]]G" } elseif {$c>(10*1000000)} { set ret "[format %d [expr $c/1000000]]M" } elseif {$c>(1000000)} { set ret "[format %.2f [expr $c/1000000.0]]M" } elseif {$c>(10*1000)} { set ret "[format %d [expr $c/1000]]K" } elseif {$c>1000} { set ret "[format %.2f [expr $c/1000.0]]K" } else { set ret $c } return $ret } # Read the qprofile information for table "v1" from eponymous virtual # table vec1cat. Print a report to stdout based on the values read. # proc print_query_timings {} { set lTopic [list] db eval { SELECT fullkey, value FROM json_tree( (SELECT qprofile FROM vec1cat WHERE name='v1') ) WHERE type='integer'; } { if {$value>0} { set key [string range $fullkey 2 end] set A($key) $value foreach {topic field} [split $key .] { if {$field=="cnt" && $topic!="total"} { lappend lTopic $topic } } } } set nTotal $A(total.cycle); set total_percent 0.0 set total_cycle 0 foreach name $lTopic { set cnt $A(${name}.cnt) set cycle $A(${name}.cycle) set percent [expr (100.0 * $cycle) / $nTotal] set total_percent [expr $total_percent + $percent] set total_cycle [expr $total_cycle + $cycle] set c [format_cycles $cycle] puts "[format {% -12s%8s%6.2f%%} $name: $c $percent] ($cnt)" } set c [format_cycles $total_cycle] puts "[format {% -12s%8s%6.2f%%} "total:" $c $total_percent]" } # Argument $t is some number of microseconds. Return a string making # this easy for humans to read - e.g. "123us" or "12.1ms". # proc format_us {t} { set ret "${t}us" if {$t>(10*1000000)} { set ret "[format %.1f [expr $t/1000000.0]]s" } elseif {$t>1000000} { set ret "[format %.2f [expr $t/1000000.0]]s" } elseif {$t>100000} { set ret "[format %d [expr $t/1000]]ms" } elseif {$t>10000} { set ret "[format %.1f [expr $t/1000.0]]ms" } elseif {$t>1000} { set ret "[format %.2f [expr $t/1000.0]]ms" } else { set ret "${t}us" } return $ret } # Add commas to integer argument $i to make it easier to read. # e.g. 12345 -> "12,345" # proc format_int {i} { set ret [list] while {$i>0} { if {$i>=1000} { set ret [concat [format %.3d [expr $i % 1000]] $ret] } else { set ret [concat $i $ret] } set i [expr $i / 1000] } join $ret , } proc my_time {caption script} { puts -nonewline "$caption..." flush stdout set t [uplevel [list time $script]] set t [lindex $t 0] puts " [format_us $t]" return $t } # Check that the __meta__ table looks ok. This command checks that there # are "train", "test", "neighbors" and "distances" tables, with types # float32, float32, int64 and float64, respectively, in the db. If not, # it prints out and error and exits. # proc check_db_looks_ok {} { db eval { WITH expect(nm, tp) AS ( SELECT 'train', 'float32' UNION ALL SELECT 'test', 'float32' UNION ALL SELECT 'neighbors', 'int64' UNION ALL SELECT 'neighbors', 'int32' ), have(nm, tp) AS ( SELECT table_name, dtype FROM __meta__ WHERE table_name IN ('train', 'test', 'neighbors') ) SELECT (count(*)==3) as 'ok' FROM have WHERE (nm, tp) IN ( SELECT nm, tp FROM expect ); } { if {$ok==0} { puts stderr "database does not look right. bailing out..." exit -1 } } } proc get_distance_function {} { set dm [db one {SELECT value FROM meta WHERE field='distance'}] set f(cosine) vec1_cos_distance set f(angular) vec1_cos_distance set f(euclidean) vec1_l2_distance set f(normalized) vec1_cos_distance if {[info exists f($dm)]==0} { puts "Unrecognized distance metric: $dm" exit 1 } return $f($dm) } # Output a summary of the db for the user to read. # proc summarize_db {} { set nQuery [db one {SELECT count(*) FROM test}] set nVector [db one {SELECT count(*) FROM train}] set nDim [db one {SELECT length(vec)/4 FROM train LIMIT 1}] set dm [db one {SELECT 'dist=' || value FROM meta WHERE field='distance'}] set cmp "$dm -> [get_distance_function]" puts "Test is $nQuery queries against $nVector ${nDim}-d vectors ($cmp)" } proc vec_to_json_i64 {blob} { binary scan $blob w* L set ret "\[[join $L ,]\]" return $ret } proc vec_to_json_i32 {blob} { binary scan $blob i* L set ret "\[[join $L ,]\]" return $ret } proc myvar {var} { upvar $var v if {[info exists v]==0} { return "" } return $v } proc print_report {} { global T global C proc vkey_cmp {lhs rhs} { foreach {lr lp} [split $lhs ,] {} foreach {rr rp} [split $rhs ,] {} if {$rr==$lr} { if {$lp<$rp} { return -1 } if {$lp>$rp} { return +1 } } return [expr $rr - $lr] } foreach k [array names T qps*] { foreach {q recall nprobe k} [split $k ,] {} set RECALL($recall) 1 set V($recall,$nprobe) 1 set H($k) 1 } if {$C(--report)==""} { set fd stdout } else { set fd [open $C(--report) a] } set nVector [format_int [db one {SELECT count(*) FROM train}]] set nSample [format_int $T(nTrain)] set nDim [db one {SELECT length(vec)/4 FROM train LIMIT 1}] set zDataset [file tail $C(dbname)] regsub {\..*} $zDataset {} zDataset puts $fd "" puts $fd "
| Parameter | Value" puts $fd " |
|---|---|
| Vec1 Version | [db one {SELECT vec1_info()}]" puts $fd " |
| Index Parameters | $T(spec)" puts $fd " |
| Training time | [format_us $T(train)] ($nSample samples, $C(--nthread) threads)" puts $fd " |
| Build time | [format_us $T(build)] ($C(--nthread) threads)" puts $fd " |
Recall@$r results:" puts $fd "" puts $fd "
| " foreach k [lsort -integer [array names K]] { puts -nonewline $fd " | K=$k" } puts $fd "" puts $fd " | |
|---|---|---|
| nprobe" foreach k [lsort -integer [array names K]] { puts -nonewline $fd " | Recall | QPS" } puts $fd "" foreach nprobe [lsort -integer [array names N]] { puts -nonewline $fd " |
| $nprobe" foreach k [lsort -integer [array names K]] { puts -nonewline $fd " | [format %.3f $T(recall,$r,$nprobe,$k)]" puts -nonewline $fd " | $T(qps,$r,$nprobe,$k)" } puts $fd "" } puts $fd " |