mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-11 22:50:00 +00:00
Format faiss Python files with black (line length 80) (#5349)
Summary: Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5349 # this is a no-op change Ran `black -l 80` over all tracked Python files under `fbcode/faiss/`, using line length 80 to match faiss's documented coding style (`CONTRIBUTING.md`: "80 character line length (both for C++ and Python)"). 172 files were reformatted; 28 were already compliant. This is a pure-formatting change. `black` runs its AST-equivalence safety check on every file, so no identifiers, string/numeric values, operators, or control flow were altered — only whitespace, indentation, line wrapping, blank lines, trailing commas, and string-quote normalization (single to double quotes). `fbcode/faiss/` is intentionally excluded from Meta's standard Python autoformatter (`arc f` / `arc lint -a` report no formatter for these paths) because it mirrors to public GitHub, so the Python had drifted out of any consistent style. The C++ is already clang-format-clean (0 of 807 files change), so this effort is Python-only. This diff brings the Python to canonical `black` formatting. Excluded `demos/index_pq_flat_separate_codes_from_codebook.py`: it contains a Jupyter `!rm` shell-magic line and is not valid Python, so `black` cannot parse it. This is the base of a 3-diff stack: (1) this black reformat; (2) `.flake8` alignment (max-line-length 80 + black-compatible ignores E203/W503); (3) genuine lint fixes (F401/F403/F841/B011). Reviewed By: mnorris11 Differential Revision: D109736099 fbshipit-source-id: 9a054635766ed11aa3760a788395babd3d198abf
This commit is contained in:
committed by
meta-codesync[bot]
parent
0fa5b0afcb
commit
0f0f728166
@@ -20,7 +20,6 @@ except ModuleNotFoundError:
|
||||
sanitize = datasets.sanitize
|
||||
|
||||
|
||||
|
||||
def unwind_index_ivf(index):
|
||||
if isinstance(index, faiss.IndexPreTransform):
|
||||
assert index.chain.size() == 1
|
||||
@@ -63,9 +62,8 @@ def apply_AQ_options(index, args):
|
||||
index.rq.use_beam_LUT = args.RQ_use_beam_LUT
|
||||
|
||||
|
||||
|
||||
def eval_setting(index, xq, gt, k, inter, min_time):
|
||||
""" evaluate searching in terms of precision vs. speed """
|
||||
"""evaluate searching in terms of precision vs. speed"""
|
||||
nq = xq.shape[0]
|
||||
ivf_stats = faiss.cvar.indexIVF_stats
|
||||
ivf_stats.reset()
|
||||
@@ -77,29 +75,29 @@ def eval_setting(index, xq, gt, k, inter, min_time):
|
||||
t1 = time.time()
|
||||
if t1 - t0 > min_time:
|
||||
break
|
||||
ms_per_query = ((t1 - t0) * 1000.0 / nq / nrun)
|
||||
res = {
|
||||
"ms_per_query": ms_per_query,
|
||||
"nrun": nrun
|
||||
}
|
||||
ms_per_query = (t1 - t0) * 1000.0 / nq / nrun
|
||||
res = {"ms_per_query": ms_per_query, "nrun": nrun}
|
||||
res["n"] = ms_per_query
|
||||
if inter:
|
||||
rank = k
|
||||
inter_measure = faiss.eval_intersection(gt[:, :rank], I[:, :rank]) / (nq * rank)
|
||||
print("%.4f" % inter_measure, end=' ')
|
||||
inter_measure = faiss.eval_intersection(gt[:, :rank], I[:, :rank]) / (
|
||||
nq * rank
|
||||
)
|
||||
print("%.4f" % inter_measure, end=" ")
|
||||
res["inter_measure"] = inter_measure
|
||||
else:
|
||||
res["recalls"] = {}
|
||||
for rank in 1, 10, 100:
|
||||
recall = (I[:, :rank] == gt[:, :1]).sum() / float(nq)
|
||||
print("%.4f" % recall, end=' ')
|
||||
print("%.4f" % recall, end=" ")
|
||||
res["recalls"][rank] = recall
|
||||
print(" %9.5f " % ms_per_query, end=' ')
|
||||
print("%12d " % (ivf_stats.ndis / nrun), end=' ')
|
||||
print(" %9.5f " % ms_per_query, end=" ")
|
||||
print("%12d " % (ivf_stats.ndis / nrun), end=" ")
|
||||
print(nrun)
|
||||
res["ndis"] = ivf_stats.ndis / nrun
|
||||
return res
|
||||
|
||||
|
||||
######################################################
|
||||
# Training
|
||||
######################################################
|
||||
@@ -109,8 +107,9 @@ def run_train(args, ds, res):
|
||||
print("build index, key=", args.indexkey)
|
||||
|
||||
index = faiss.index_factory(
|
||||
ds.d, args.indexkey, faiss.METRIC_L2 if ds.metric == "L2" else
|
||||
faiss.METRIC_INNER_PRODUCT
|
||||
ds.d,
|
||||
args.indexkey,
|
||||
faiss.METRIC_L2 if ds.metric == "L2" else faiss.METRIC_INNER_PRODUCT,
|
||||
)
|
||||
|
||||
index_ivf, vec_transform = unwind_index_ivf(index)
|
||||
@@ -118,7 +117,7 @@ def run_train(args, ds, res):
|
||||
if args.by_residual != -1:
|
||||
by_residual = args.by_residual == 1
|
||||
print("setting by_residual = ", by_residual)
|
||||
index_ivf.by_residual # check if field exists
|
||||
index_ivf.by_residual # check if field exists
|
||||
index_ivf.by_residual = by_residual
|
||||
|
||||
if index_ivf:
|
||||
@@ -127,16 +126,21 @@ def run_train(args, ds, res):
|
||||
# because otherwise the assignment is inaccurate
|
||||
quantizer = faiss.downcast_index(index_ivf.quantizer)
|
||||
if isinstance(quantizer, faiss.IndexRefine):
|
||||
print(" update quantizer k_factor=", quantizer.k_factor, end=" -> ")
|
||||
print(
|
||||
" update quantizer k_factor=", quantizer.k_factor, end=" -> "
|
||||
)
|
||||
quantizer.k_factor = 32 if index_ivf.nlist < 1e6 else 64
|
||||
print(quantizer.k_factor)
|
||||
base_index = faiss.downcast_index(quantizer.base_index)
|
||||
if isinstance(base_index, faiss.IndexIVF):
|
||||
print(" update quantizer nprobe=", base_index.nprobe, end=" -> ")
|
||||
print(
|
||||
" update quantizer nprobe=", base_index.nprobe, end=" -> "
|
||||
)
|
||||
base_index.nprobe = (
|
||||
16 if base_index.nlist < 1e5 else
|
||||
32 if base_index.nlist < 4e6 else
|
||||
64)
|
||||
16
|
||||
if base_index.nlist < 1e5
|
||||
else 32 if base_index.nlist < 4e6 else 64
|
||||
)
|
||||
print(base_index.nprobe)
|
||||
elif isinstance(quantizer, faiss.IndexHNSW):
|
||||
hnsw = quantizer.hnsw
|
||||
@@ -159,7 +163,7 @@ def run_train(args, ds, res):
|
||||
|
||||
maxtrain = args.maxtrain
|
||||
if maxtrain == 0:
|
||||
if 'IMI' in args.indexkey:
|
||||
if "IMI" in args.indexkey:
|
||||
maxtrain = int(256 * 2 ** (np.log2(index_ivf.nlist) / 2))
|
||||
elif index_ivf:
|
||||
maxtrain = 50 * index_ivf.nlist
|
||||
@@ -178,28 +182,31 @@ def run_train(args, ds, res):
|
||||
print("train, size", xt2.shape)
|
||||
assert np.all(np.isfinite(xt2))
|
||||
|
||||
if (isinstance(vec_transform, faiss.OPQMatrix) and
|
||||
isinstance(index_ivf, faiss.IndexIVFPQFastScan)):
|
||||
if isinstance(vec_transform, faiss.OPQMatrix) and isinstance(
|
||||
index_ivf, faiss.IndexIVFPQFastScan
|
||||
):
|
||||
print(" Forcing OPQ training PQ to PQ4")
|
||||
ref_pq = index_ivf.pq
|
||||
training_pq = faiss.ProductQuantizer(
|
||||
ref_pq.d, ref_pq.M, ref_pq.nbits
|
||||
)
|
||||
training_pq = faiss.ProductQuantizer(ref_pq.d, ref_pq.M, ref_pq.nbits)
|
||||
vec_transform.pq
|
||||
vec_transform.pq = training_pq
|
||||
|
||||
|
||||
if args.get_centroids_from == '':
|
||||
if args.get_centroids_from == "":
|
||||
|
||||
if args.clustering_niter >= 0:
|
||||
print(("setting nb of clustering iterations to %d" %
|
||||
args.clustering_niter))
|
||||
print(
|
||||
(
|
||||
"setting nb of clustering iterations to %d"
|
||||
% args.clustering_niter
|
||||
)
|
||||
)
|
||||
index_ivf.cp.niter = args.clustering_niter
|
||||
|
||||
if args.train_on_gpu:
|
||||
print("add a training index on GPU")
|
||||
train_index = faiss.index_cpu_to_all_gpus(
|
||||
faiss.IndexFlatL2(index_ivf.d))
|
||||
faiss.IndexFlatL2(index_ivf.d)
|
||||
)
|
||||
index_ivf.clustering_index = train_index
|
||||
|
||||
else:
|
||||
@@ -229,6 +236,7 @@ def run_train(args, ds, res):
|
||||
print(" train in %.3f s" % res.train_time)
|
||||
return index
|
||||
|
||||
|
||||
######################################################
|
||||
# Populating index
|
||||
######################################################
|
||||
@@ -242,14 +250,15 @@ def run_add(args, ds, index, res):
|
||||
assert args.split == [1, 0], "split not supported with full batch add"
|
||||
index.add(sanitize(ds.get_database()))
|
||||
else:
|
||||
totn = ds.nb // args.split[0] # approximate
|
||||
totn = ds.nb // args.split[0] # approximate
|
||||
i0 = 0
|
||||
print(f"Adding in block sizes {args.add_bs} with split {args.split}")
|
||||
for xblock in ds.database_iterator(bs=args.add_bs, split=args.split):
|
||||
i1 = i0 + len(xblock)
|
||||
print(" adding %d:%d / %d [%.3f s, RSS %d kiB] " % (
|
||||
i0, i1, totn, time.time() - t0,
|
||||
faiss.get_mem_usage_kb()))
|
||||
print(
|
||||
" adding %d:%d / %d [%.3f s, RSS %d kiB] "
|
||||
% (i0, i1, totn, time.time() - t0, faiss.get_mem_usage_kb())
|
||||
)
|
||||
index.add(xblock)
|
||||
i0 = i1
|
||||
|
||||
@@ -261,6 +270,7 @@ def run_add(args, ds, index, res):
|
||||
# Search
|
||||
######################################################
|
||||
|
||||
|
||||
def run_search(args, ds, index, res):
|
||||
|
||||
index_ivf, vec_transform = unwind_index_ivf(index)
|
||||
@@ -283,7 +293,7 @@ def run_search(args, ds, index, res):
|
||||
print("current RSS:", faiss.get_mem_usage_kb() * 1024)
|
||||
|
||||
precomputed_table_size = 0
|
||||
if hasattr(index_ivf, 'precomputed_table'):
|
||||
if hasattr(index_ivf, "precomputed_table"):
|
||||
precomputed_table_size = index_ivf.precomputed_table.size() * 4
|
||||
|
||||
print("precomputed tables size:", precomputed_table_size)
|
||||
@@ -294,7 +304,7 @@ def run_search(args, ds, index, res):
|
||||
nq, d = xq.shape
|
||||
gt = ds.get_groundtruth(k=args.k)
|
||||
|
||||
if not args.accept_short_gt: # Deep1B has only a single NN per query
|
||||
if not args.accept_short_gt: # Deep1B has only a single NN per query
|
||||
assert gt.shape[1] == args.k
|
||||
|
||||
if args.searchthreads != -1:
|
||||
@@ -309,26 +319,25 @@ def run_search(args, ds, index, res):
|
||||
parametersets = args.searchparams
|
||||
|
||||
if args.inter:
|
||||
header = (
|
||||
'%-40s inter@%3d time(ms/q) nb distances #runs' %
|
||||
("parameters", args.k)
|
||||
header = "%-40s inter@%3d time(ms/q) nb distances #runs" % (
|
||||
"parameters",
|
||||
args.k,
|
||||
)
|
||||
else:
|
||||
|
||||
header = (
|
||||
'%-40s R@1 R@10 R@100 time(ms/q) nb distances #runs' %
|
||||
"parameters"
|
||||
"%-40s R@1 R@10 R@100 time(ms/q) nb distances #runs"
|
||||
% "parameters"
|
||||
)
|
||||
|
||||
|
||||
res.search_results = {}
|
||||
if parametersets == ['autotune']:
|
||||
if parametersets == ["autotune"]:
|
||||
|
||||
ps.n_experiments = args.n_autotune
|
||||
ps.min_test_duration = args.min_test_duration
|
||||
|
||||
for kv in args.autotune_max:
|
||||
k, vmax = kv.split(':')
|
||||
k, vmax = kv.split(":")
|
||||
vmax = float(vmax)
|
||||
print("limiting %s to %g" % (k, vmax))
|
||||
pr = ps.add_range(k)
|
||||
@@ -337,8 +346,8 @@ def run_search(args, ds, index, res):
|
||||
faiss.copy_array_to_vector(values, pr.values)
|
||||
|
||||
for kv in args.autotune_range:
|
||||
k, vals = kv.split(':')
|
||||
vals = np.fromstring(vals, sep=',')
|
||||
k, vals = kv.split(":")
|
||||
vals = np.fromstring(vals, sep=",")
|
||||
print("setting %s to %s" % (k, vals))
|
||||
pr = ps.add_range(k)
|
||||
faiss.copy_array_to_vector(vals, pr.values)
|
||||
@@ -353,10 +362,13 @@ def run_search(args, ds, index, res):
|
||||
|
||||
# by default, the criterion will request only 1 NN
|
||||
crit.nnn = args.k
|
||||
crit.set_groundtruth(None, gt.astype('int64'))
|
||||
crit.set_groundtruth(None, gt.astype("int64"))
|
||||
|
||||
# then we let Faiss find the optimal parameters by itself
|
||||
print("exploring operating points, %d threads" % faiss.omp_get_max_threads());
|
||||
print(
|
||||
"exploring operating points, %d threads"
|
||||
% faiss.omp_get_max_threads()
|
||||
)
|
||||
ps.display()
|
||||
|
||||
t0 = time.time()
|
||||
@@ -375,24 +387,27 @@ def run_search(args, ds, index, res):
|
||||
|
||||
ps.set_index_parameters(index, opt.key)
|
||||
|
||||
print(opt.key.ljust(maxw), end=' ')
|
||||
print(opt.key.ljust(maxw), end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
res_i = eval_setting(index, xq, gt, args.k, args.inter, args.min_test_duration)
|
||||
res_i = eval_setting(
|
||||
index, xq, gt, args.k, args.inter, args.min_test_duration
|
||||
)
|
||||
res.search_results[opt.key] = res_i
|
||||
|
||||
else:
|
||||
print(header)
|
||||
for param in parametersets:
|
||||
print("%-40s " % param, end=' ')
|
||||
print("%-40s " % param, end=" ")
|
||||
sys.stdout.flush()
|
||||
ps.set_index_parameters(index, param)
|
||||
|
||||
res_i = eval_setting(index, xq, gt, args.k, args.inter, args.min_test_duration)
|
||||
res_i = eval_setting(
|
||||
index, xq, gt, args.k, args.inter, args.min_test_duration
|
||||
)
|
||||
res.search_results[param] = res_i
|
||||
|
||||
|
||||
|
||||
######################################################
|
||||
# Driver function
|
||||
######################################################
|
||||
@@ -405,76 +420,173 @@ def main():
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('general options')
|
||||
aa('--nthreads', default=-1, type=int,
|
||||
help='nb of threads to use at train and add time')
|
||||
aa('--json', default=False, action="store_true",
|
||||
help="output stats in JSON format at the end")
|
||||
aa('--todo', default=["check_files"],
|
||||
choices=["train", "add", "search", "check_files"],
|
||||
nargs="+", help='what to do (check_files means decide depending on which index files exist)')
|
||||
group = parser.add_argument_group("general options")
|
||||
aa(
|
||||
"--nthreads",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="nb of threads to use at train and add time",
|
||||
)
|
||||
aa(
|
||||
"--json",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="output stats in JSON format at the end",
|
||||
)
|
||||
aa(
|
||||
"--todo",
|
||||
default=["check_files"],
|
||||
choices=["train", "add", "search", "check_files"],
|
||||
nargs="+",
|
||||
help="what to do (check_files means decide depending on which index files exist)",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('dataset options')
|
||||
aa('--db', default='deep1M', help='dataset')
|
||||
aa('--compute_gt', default=False, action='store_true',
|
||||
help='compute and store the groundtruth')
|
||||
aa('--force_IP', default=False, action="store_true",
|
||||
help='force IP search instead of L2')
|
||||
aa('--accept_short_gt', default=False, action='store_true',
|
||||
help='work around a problem with Deep1B GT')
|
||||
group = parser.add_argument_group("dataset options")
|
||||
aa("--db", default="deep1M", help="dataset")
|
||||
aa(
|
||||
"--compute_gt",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="compute and store the groundtruth",
|
||||
)
|
||||
aa(
|
||||
"--force_IP",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="force IP search instead of L2",
|
||||
)
|
||||
aa(
|
||||
"--accept_short_gt",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="work around a problem with Deep1B GT",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('index construction')
|
||||
aa('--indexkey', default='HNSW32', help='index_factory type')
|
||||
aa('--trained_indexfile', default='',
|
||||
help='file to read or write a trained index from')
|
||||
aa('--maxtrain', default=256 * 256, type=int,
|
||||
help='maximum number of training points (0 to set automatically)')
|
||||
aa('--indexfile', default='', help='file to read or write index from')
|
||||
aa('--split', default=[1, 0], type=int, nargs=2, help="database split")
|
||||
aa('--add_bs', default=-1, type=int,
|
||||
help='add elements index by batches of this size')
|
||||
group = parser.add_argument_group("index construction")
|
||||
aa("--indexkey", default="HNSW32", help="index_factory type")
|
||||
aa(
|
||||
"--trained_indexfile",
|
||||
default="",
|
||||
help="file to read or write a trained index from",
|
||||
)
|
||||
aa(
|
||||
"--maxtrain",
|
||||
default=256 * 256,
|
||||
type=int,
|
||||
help="maximum number of training points (0 to set automatically)",
|
||||
)
|
||||
aa("--indexfile", default="", help="file to read or write index from")
|
||||
aa("--split", default=[1, 0], type=int, nargs=2, help="database split")
|
||||
aa(
|
||||
"--add_bs",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="add elements index by batches of this size",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('IVF options')
|
||||
aa('--by_residual', default=-1, type=int,
|
||||
help="set if index should use residuals (default=unchanged)")
|
||||
aa('--no_precomputed_tables', action='store_true', default=False,
|
||||
help='disable precomputed tables (uses less memory)')
|
||||
aa('--get_centroids_from', default='',
|
||||
help='get the centroids from this index (to speed up training)')
|
||||
aa('--clustering_niter', default=-1, type=int,
|
||||
help='number of clustering iterations (-1 = leave default)')
|
||||
aa('--train_on_gpu', default=False, action='store_true',
|
||||
help='do training on GPU')
|
||||
group = parser.add_argument_group("IVF options")
|
||||
aa(
|
||||
"--by_residual",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="set if index should use residuals (default=unchanged)",
|
||||
)
|
||||
aa(
|
||||
"--no_precomputed_tables",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="disable precomputed tables (uses less memory)",
|
||||
)
|
||||
aa(
|
||||
"--get_centroids_from",
|
||||
default="",
|
||||
help="get the centroids from this index (to speed up training)",
|
||||
)
|
||||
aa(
|
||||
"--clustering_niter",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="number of clustering iterations (-1 = leave default)",
|
||||
)
|
||||
aa(
|
||||
"--train_on_gpu",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="do training on GPU",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('index-specific options')
|
||||
aa('--M0', default=-1, type=int, help='size of base level for HNSW')
|
||||
aa('--RQ_train_default', default=False, action="store_true",
|
||||
help='disable progressive dim training for RQ')
|
||||
aa('--RQ_beam_size', default=-1, type=int,
|
||||
help='set beam size at add time')
|
||||
aa('--LSQ_encode_ils_iters', default=-1, type=int,
|
||||
help='ILS iterations for LSQ')
|
||||
aa('--RQ_use_beam_LUT', default=-1, type=int,
|
||||
help='use beam LUT at add time')
|
||||
group = parser.add_argument_group("index-specific options")
|
||||
aa("--M0", default=-1, type=int, help="size of base level for HNSW")
|
||||
aa(
|
||||
"--RQ_train_default",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="disable progressive dim training for RQ",
|
||||
)
|
||||
aa("--RQ_beam_size", default=-1, type=int, help="set beam size at add time")
|
||||
aa(
|
||||
"--LSQ_encode_ils_iters",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="ILS iterations for LSQ",
|
||||
)
|
||||
aa(
|
||||
"--RQ_use_beam_LUT",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="use beam LUT at add time",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('searching')
|
||||
aa('--k', default=100, type=int, help='nb of nearest neighbors')
|
||||
aa('--inter', default=False, action='store_true',
|
||||
help='use intersection measure instead of 1-recall as metric')
|
||||
aa('--searchthreads', default=-1, type=int,
|
||||
help='nb of threads to use at search time')
|
||||
aa('--searchparams', nargs='+', default=['autotune'],
|
||||
help="search parameters to use (can be autotune or a list of params)")
|
||||
aa('--n_autotune', default=500, type=int,
|
||||
help="max nb of autotune experiments")
|
||||
aa('--autotune_max', default=[], nargs='*',
|
||||
help='set max value for autotune variables format "var:val" (exclusive)')
|
||||
aa('--autotune_range', default=[], nargs='*',
|
||||
help='set complete autotune range, format "var:val1,val2,..."')
|
||||
aa('--min_test_duration', default=3.0, type=float,
|
||||
help='run test at least for so long to avoid jitter')
|
||||
aa('--indexes_to_merge', default=[], nargs="*",
|
||||
help="load these indexes to search and merge them before searching")
|
||||
group = parser.add_argument_group("searching")
|
||||
aa("--k", default=100, type=int, help="nb of nearest neighbors")
|
||||
aa(
|
||||
"--inter",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="use intersection measure instead of 1-recall as metric",
|
||||
)
|
||||
aa(
|
||||
"--searchthreads",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="nb of threads to use at search time",
|
||||
)
|
||||
aa(
|
||||
"--searchparams",
|
||||
nargs="+",
|
||||
default=["autotune"],
|
||||
help="search parameters to use (can be autotune or a list of params)",
|
||||
)
|
||||
aa(
|
||||
"--n_autotune",
|
||||
default=500,
|
||||
type=int,
|
||||
help="max nb of autotune experiments",
|
||||
)
|
||||
aa(
|
||||
"--autotune_max",
|
||||
default=[],
|
||||
nargs="*",
|
||||
help='set max value for autotune variables format "var:val" (exclusive)',
|
||||
)
|
||||
aa(
|
||||
"--autotune_range",
|
||||
default=[],
|
||||
nargs="*",
|
||||
help='set complete autotune range, format "var:val1,val2,..."',
|
||||
)
|
||||
aa(
|
||||
"--min_test_duration",
|
||||
default=3.0,
|
||||
type=float,
|
||||
help="run test at least for so long to avoid jitter",
|
||||
)
|
||||
aa(
|
||||
"--indexes_to_merge",
|
||||
default=[],
|
||||
nargs="*",
|
||||
help="load these indexes to search and merge them before searching",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -489,22 +601,23 @@ def main():
|
||||
|
||||
print("args:", args)
|
||||
|
||||
os.system('echo -n "nb processors "; '
|
||||
'cat /proc/cpuinfo | grep ^processor | wc -l; '
|
||||
'cat /proc/cpuinfo | grep ^"model name" | tail -1')
|
||||
os.system(
|
||||
'echo -n "nb processors "; '
|
||||
"cat /proc/cpuinfo | grep ^processor | wc -l; "
|
||||
'cat /proc/cpuinfo | grep ^"model name" | tail -1'
|
||||
)
|
||||
|
||||
# object to collect results
|
||||
res = argparse.Namespace()
|
||||
res.args = args.__dict__
|
||||
|
||||
res.cpu_model = [
|
||||
l for l in open("/proc/cpuinfo", "r")
|
||||
if "model name" in l][0]
|
||||
l for l in open("/proc/cpuinfo", "r") if "model name" in l
|
||||
][0]
|
||||
|
||||
print("Load dataset")
|
||||
|
||||
ds = datasets.load_dataset(
|
||||
dataset=args.db, compute_gt=args.compute_gt)
|
||||
ds = datasets.load_dataset(dataset=args.db, compute_gt=args.compute_gt)
|
||||
|
||||
if args.force_IP:
|
||||
ds.metric = "IP"
|
||||
|
||||
@@ -21,28 +21,30 @@ def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
group = parser.add_argument_group('dataset options')
|
||||
group = parser.add_argument_group("dataset options")
|
||||
|
||||
aa('--db', default='deep1M', help='dataset')
|
||||
aa('--nt', default=65536, type=int)
|
||||
aa('--nb', default=100000, type=int)
|
||||
aa('--nt_sample', default=0, type=int)
|
||||
aa("--db", default="deep1M", help="dataset")
|
||||
aa("--nt", default=65536, type=int)
|
||||
aa("--nb", default=100000, type=int)
|
||||
aa("--nt_sample", default=0, type=int)
|
||||
|
||||
group = parser.add_argument_group('kmeans options')
|
||||
aa('--k', default=256, type=int)
|
||||
aa('--seed', default=12345, type=int)
|
||||
aa('--pcadim', default=-1, type=int, help='PCA to this dimension')
|
||||
aa('--niter', default=25, type=int)
|
||||
aa('--eval_freq', default=100, type=int)
|
||||
group = parser.add_argument_group("kmeans options")
|
||||
aa("--k", default=256, type=int)
|
||||
aa("--seed", default=12345, type=int)
|
||||
aa("--pcadim", default=-1, type=int, help="PCA to this dimension")
|
||||
aa("--niter", default=25, type=int)
|
||||
aa("--eval_freq", default=100, type=int)
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("args:", args)
|
||||
|
||||
os.system('echo -n "nb processors "; '
|
||||
'cat /proc/cpuinfo | grep ^processor | wc -l; '
|
||||
'cat /proc/cpuinfo | grep ^"model name" | tail -1')
|
||||
os.system(
|
||||
'echo -n "nb processors "; '
|
||||
"cat /proc/cpuinfo | grep ^processor | wc -l; "
|
||||
'cat /proc/cpuinfo | grep ^"model name" | tail -1'
|
||||
)
|
||||
|
||||
ngpu = faiss.get_num_gpus()
|
||||
print("nb GPUs:", ngpu)
|
||||
@@ -55,15 +57,15 @@ xt, xb, xq, gt = datasets.load_data(dataset=args.db)
|
||||
|
||||
|
||||
if args.nt_sample == 0:
|
||||
xt_pca = xt[args.nt:args.nt + 10000]
|
||||
xt = xt[:args.nt]
|
||||
xt_pca = xt[args.nt : args.nt + 10000]
|
||||
xt = xt[: args.nt]
|
||||
else:
|
||||
xt_pca = xt[args.nt_sample:args.nt_sample + 10000]
|
||||
xt_pca = xt[args.nt_sample : args.nt_sample + 10000]
|
||||
rs = np.random.RandomState(args.seed)
|
||||
idx = rs.choice(args.nt_sample, size=args.nt, replace=False)
|
||||
xt = xt[idx]
|
||||
|
||||
xb = xb[:args.nb]
|
||||
xb = xb[: args.nb]
|
||||
|
||||
d = xb.shape[1]
|
||||
|
||||
|
||||
@@ -47,31 +47,38 @@ def main():
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('dataset options')
|
||||
group = parser.add_argument_group("dataset options")
|
||||
|
||||
aa('--db', default='deep1M', help='dataset')
|
||||
aa('--measure', default="1-recall",
|
||||
help="perf measure to use: 1-recall or inter")
|
||||
aa('--download', default=False, action="store_true")
|
||||
aa('--lib', default='faiss', help='library to use (faiss or scann)')
|
||||
aa('--thenscann', default=False, action="store_true")
|
||||
aa('--base_dir',
|
||||
default='/checkpoint/matthijs/faiss_improvements/cmp_ivf_scan_2')
|
||||
aa("--db", default="deep1M", help="dataset")
|
||||
aa(
|
||||
"--measure",
|
||||
default="1-recall",
|
||||
help="perf measure to use: 1-recall or inter",
|
||||
)
|
||||
aa("--download", default=False, action="store_true")
|
||||
aa("--lib", default="faiss", help="library to use (faiss or scann)")
|
||||
aa("--thenscann", default=False, action="store_true")
|
||||
aa(
|
||||
"--base_dir",
|
||||
default="/checkpoint/matthijs/faiss_improvements/cmp_ivf_scan_2",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('searching')
|
||||
aa('--k', default=10, type=int, help='nb of nearest neighbors')
|
||||
aa('--pre_reorder_k', default="0,10,100,1000", help='values for reorder_k')
|
||||
aa('--nprobe', default="1,2,5,10,20,50,100,200", help='values for nprobe')
|
||||
aa('--nrun', default=5, type=int, help='nb of runs to perform')
|
||||
group = parser.add_argument_group("searching")
|
||||
aa("--k", default=10, type=int, help="nb of nearest neighbors")
|
||||
aa("--pre_reorder_k", default="0,10,100,1000", help="values for reorder_k")
|
||||
aa("--nprobe", default="1,2,5,10,20,50,100,200", help="values for nprobe")
|
||||
aa("--nrun", default=5, type=int, help="nb of runs to perform")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("args:", args)
|
||||
pre_reorder_k_tab = [int(x) for x in args.pre_reorder_k.split(',')]
|
||||
nprobe_tab = [int(x) for x in args.nprobe.split(',')]
|
||||
pre_reorder_k_tab = [int(x) for x in args.pre_reorder_k.split(",")]
|
||||
nprobe_tab = [int(x) for x in args.nprobe.split(",")]
|
||||
|
||||
os.system('echo -n "nb processors "; '
|
||||
'cat /proc/cpuinfo | grep ^processor | wc -l; '
|
||||
'cat /proc/cpuinfo | grep ^"model name" | tail -1')
|
||||
os.system(
|
||||
'echo -n "nb processors "; '
|
||||
"cat /proc/cpuinfo | grep ^processor | wc -l; "
|
||||
'cat /proc/cpuinfo | grep ^"model name" | tail -1'
|
||||
)
|
||||
|
||||
cache_dir = args.base_dir + "/" + args.db + "/"
|
||||
k = args.k
|
||||
@@ -80,6 +87,7 @@ def main():
|
||||
if not os.path.exists(cache_dir + "xb.npy"):
|
||||
# prepare cache
|
||||
from datasets import load_dataset
|
||||
|
||||
ds = load_dataset(args.db, download=args.download)
|
||||
print(ds)
|
||||
# store for SCANN
|
||||
@@ -95,7 +103,7 @@ def main():
|
||||
np.save(fname, v)
|
||||
|
||||
open(cache_dir + "metric", "w").write(ds.metric)
|
||||
|
||||
|
||||
dataset = {}
|
||||
for kn in "xb xq gt".split():
|
||||
fname = cache_dir + "/" + kn + ".npy"
|
||||
@@ -103,55 +111,69 @@ def main():
|
||||
dataset[kn] = np.load(fname)
|
||||
xb = dataset["xb"]
|
||||
xq = dataset["xq"]
|
||||
gt = dataset["gt"]
|
||||
gt = dataset["gt"]
|
||||
distance_measure = open(cache_dir + "metric").read()
|
||||
|
||||
|
||||
if args.lib == "faiss":
|
||||
import faiss
|
||||
|
||||
name1_to_metric = {
|
||||
"IP": faiss.METRIC_INNER_PRODUCT,
|
||||
"L2": faiss.METRIC_L2
|
||||
"L2": faiss.METRIC_L2,
|
||||
}
|
||||
|
||||
index_fname = cache_dir + "index.faiss"
|
||||
if not os.path.exists(index_fname):
|
||||
index = faiss_make_index(
|
||||
xb, name1_to_metric[distance_measure], index_fname)
|
||||
xb, name1_to_metric[distance_measure], index_fname
|
||||
)
|
||||
else:
|
||||
index = faiss.read_index(index_fname)
|
||||
|
||||
faiss_eval_search(
|
||||
index, xq, xb, nprobe_tab, pre_reorder_k_tab, k, gt,
|
||||
nrun, args.measure
|
||||
index,
|
||||
xq,
|
||||
xb,
|
||||
nprobe_tab,
|
||||
pre_reorder_k_tab,
|
||||
k,
|
||||
gt,
|
||||
nrun,
|
||||
args.measure,
|
||||
)
|
||||
|
||||
if args.lib == "scann":
|
||||
from scann.scann_ops.py import scann_ops_pybind
|
||||
|
||||
name1_to_name2 = {
|
||||
"IP": "dot_product",
|
||||
"L2": "squared_l2"
|
||||
}
|
||||
name1_to_name2 = {"IP": "dot_product", "L2": "squared_l2"}
|
||||
|
||||
scann_dir = cache_dir + "/scann1.1.1_serialized"
|
||||
if os.path.exists(scann_dir + "/scann_config.pb"):
|
||||
searcher = scann_ops_pybind.load_searcher(scann_dir)
|
||||
else:
|
||||
searcher = scann_make_index(
|
||||
xb, name1_to_name2[distance_measure], scann_dir, 0)
|
||||
xb, name1_to_name2[distance_measure], scann_dir, 0
|
||||
)
|
||||
|
||||
scann_dir = cache_dir + "/scann1.1.1_serialized_reorder"
|
||||
if os.path.exists(scann_dir + "/scann_config.pb"):
|
||||
searcher_reo = scann_ops_pybind.load_searcher(scann_dir)
|
||||
else:
|
||||
searcher_reo = scann_make_index(
|
||||
xb, name1_to_name2[distance_measure], scann_dir, 100)
|
||||
xb, name1_to_name2[distance_measure], scann_dir, 100
|
||||
)
|
||||
|
||||
scann_eval_search(
|
||||
searcher, searcher_reo,
|
||||
xq, xb, nprobe_tab, pre_reorder_k_tab, k, gt,
|
||||
nrun, args.measure
|
||||
searcher,
|
||||
searcher_reo,
|
||||
xq,
|
||||
xb,
|
||||
nprobe_tab,
|
||||
pre_reorder_k_tab,
|
||||
k,
|
||||
gt,
|
||||
nrun,
|
||||
args.measure,
|
||||
)
|
||||
|
||||
if args.lib != "scann" and args.thenscann:
|
||||
@@ -159,9 +181,10 @@ def main():
|
||||
# options
|
||||
cmdline = " ".join(sys.argv) + " --lib scann"
|
||||
cmdline = (
|
||||
". ~/anaconda3/etc/profile.d/conda.sh ; " +
|
||||
"conda activate scann_1.1.1; "
|
||||
"python -u " + cmdline)
|
||||
". ~/anaconda3/etc/profile.d/conda.sh ; "
|
||||
+ "conda activate scann_1.1.1; "
|
||||
"python -u " + cmdline
|
||||
)
|
||||
|
||||
print("running", cmdline)
|
||||
|
||||
@@ -183,10 +206,10 @@ def scann_make_index(xb, distance_measure, scann_dir, reorder_k):
|
||||
else:
|
||||
thr = 0
|
||||
k = 10
|
||||
sb = scann.scann_ops_pybind.builder(
|
||||
xb, k, distance_measure)
|
||||
sb = scann.scann_ops_pybind.builder(xb, k, distance_measure)
|
||||
sb = sb.tree(
|
||||
num_leaves=2000, num_leaves_to_search=100, training_sample_size=250000)
|
||||
num_leaves=2000, num_leaves_to_search=100, training_sample_size=250000
|
||||
)
|
||||
sb = sb.score_ah(2, anisotropic_quantization_threshold=thr)
|
||||
|
||||
if reorder_k > 0:
|
||||
@@ -205,9 +228,17 @@ def scann_make_index(xb, distance_measure, scann_dir, reorder_k):
|
||||
|
||||
|
||||
def scann_eval_search(
|
||||
searcher, searcher_reo,
|
||||
xq, xb, nprobe_tab, pre_reorder_k_tab, k, gt,
|
||||
nrun, measure):
|
||||
searcher,
|
||||
searcher_reo,
|
||||
xq,
|
||||
xb,
|
||||
nprobe_tab,
|
||||
pre_reorder_k_tab,
|
||||
k,
|
||||
gt,
|
||||
nrun,
|
||||
measure,
|
||||
):
|
||||
|
||||
# warmup
|
||||
for _run in range(5):
|
||||
@@ -228,8 +259,10 @@ def scann_eval_search(
|
||||
else:
|
||||
t0 = time.time()
|
||||
I, D = searcher_reo.search_batched(
|
||||
xq, leaves_to_search=nprobe, final_num_neighbors=k,
|
||||
pre_reorder_num_neighbors=pre_reorder_k
|
||||
xq,
|
||||
leaves_to_search=nprobe,
|
||||
final_num_neighbors=k,
|
||||
pre_reorder_num_neighbors=pre_reorder_k,
|
||||
)
|
||||
t1 = time.time()
|
||||
|
||||
@@ -267,13 +300,16 @@ def faiss_make_index(xb, metric_type, fname):
|
||||
|
||||
|
||||
def faiss_eval_search(
|
||||
index, xq, xb, nprobe_tab, pre_reorder_k_tab,
|
||||
k, gt, nrun, measure
|
||||
):
|
||||
index, xq, xb, nprobe_tab, pre_reorder_k_tab, k, gt, nrun, measure
|
||||
):
|
||||
import faiss
|
||||
|
||||
print("use precomputed table=", index.use_precomputed_table,
|
||||
"by residual=", index.by_residual)
|
||||
print(
|
||||
"use precomputed table=",
|
||||
index.use_precomputed_table,
|
||||
"by residual=",
|
||||
index.by_residual,
|
||||
)
|
||||
|
||||
print("adding a refine index")
|
||||
index_refine = faiss.IndexRefineFlat(index, faiss.swig_ptr(xb))
|
||||
|
||||
@@ -15,11 +15,11 @@ from faiss.contrib import datasets as faiss_datasets
|
||||
|
||||
print("path:", faiss_datasets.__file__)
|
||||
|
||||
faiss_datasets.dataset_basedir = '/checkpoint/matthijs/simsearch/'
|
||||
faiss_datasets.dataset_basedir = "/checkpoint/matthijs/simsearch/"
|
||||
|
||||
|
||||
def sanitize(x):
|
||||
return np.ascontiguousarray(x, dtype='float32')
|
||||
return np.ascontiguousarray(x, dtype="float32")
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -53,44 +53,46 @@ class DatasetCentroids(faiss_datasets.Dataset):
|
||||
|
||||
def get_groundtruth(self, k=100):
|
||||
return faiss.knn(
|
||||
self.xq, self.xb, k,
|
||||
faiss.METRIC_L2 if self.metric == 'L2' else faiss.METRIC_INNER_PRODUCT
|
||||
self.xq,
|
||||
self.xb,
|
||||
k,
|
||||
(
|
||||
faiss.METRIC_L2
|
||||
if self.metric == "L2"
|
||||
else faiss.METRIC_INNER_PRODUCT
|
||||
),
|
||||
)[1]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def load_dataset(dataset='deep1M', compute_gt=False, download=False):
|
||||
def load_dataset(dataset="deep1M", compute_gt=False, download=False):
|
||||
|
||||
print("load data", dataset)
|
||||
|
||||
if dataset == 'sift1M':
|
||||
if dataset == "sift1M":
|
||||
return faiss_datasets.DatasetSIFT1M()
|
||||
|
||||
elif dataset.startswith('bigann'):
|
||||
elif dataset.startswith("bigann"):
|
||||
|
||||
dbsize = 1000 if dataset == "bigann1B" else int(dataset[6:-1])
|
||||
|
||||
return faiss_datasets.DatasetBigANN(nb_M=dbsize)
|
||||
|
||||
elif dataset.startswith("deep_centroids_"):
|
||||
ncent = int(dataset[len("deep_centroids_"):])
|
||||
ncent = int(dataset[len("deep_centroids_") :])
|
||||
centdir = "/checkpoint/matthijs/bench_all_ivf/precomputed_clusters"
|
||||
return DatasetCentroids(
|
||||
faiss_datasets.DatasetDeep1B(nb=1000000),
|
||||
f"{centdir}/clustering.dbdeep1M.IVF{ncent}.faissindex"
|
||||
f"{centdir}/clustering.dbdeep1M.IVF{ncent}.faissindex",
|
||||
)
|
||||
|
||||
elif dataset.startswith("deep"):
|
||||
|
||||
szsuf = dataset[4:]
|
||||
if szsuf[-1] == 'M':
|
||||
dbsize = 10 ** 6 * int(szsuf[:-1])
|
||||
elif szsuf == '1B':
|
||||
dbsize = 10 ** 9
|
||||
elif szsuf[-1] == 'k':
|
||||
if szsuf[-1] == "M":
|
||||
dbsize = 10**6 * int(szsuf[:-1])
|
||||
elif szsuf == "1B":
|
||||
dbsize = 10**9
|
||||
elif szsuf[-1] == "k":
|
||||
dbsize = 1000 * int(szsuf[:-1])
|
||||
else:
|
||||
assert False, "did not recognize suffix " + szsuf
|
||||
@@ -117,7 +119,7 @@ def evaluate_DI(D, I, gt):
|
||||
rank = 1
|
||||
while rank <= k:
|
||||
recall = (I[:, :rank] == gt[:, :1]).sum() / float(nq)
|
||||
print("R@%d: %.4f" % (rank, recall), end=' ')
|
||||
print("R@%d: %.4f" % (rank, recall), end=" ")
|
||||
rank *= 10
|
||||
|
||||
|
||||
@@ -126,12 +128,11 @@ def evaluate(xq, gt, index, k=100, endl=True):
|
||||
D, I = index.search(xq, k)
|
||||
t1 = time.time()
|
||||
nq = xq.shape[0]
|
||||
print("\t %8.4f ms per query, " % (
|
||||
(t1 - t0) * 1000.0 / nq), end=' ')
|
||||
print("\t %8.4f ms per query, " % ((t1 - t0) * 1000.0 / nq), end=" ")
|
||||
rank = 1
|
||||
while rank <= k:
|
||||
recall = (I[:, :rank] == gt[:, :1]).sum() / float(nq)
|
||||
print("R@%d: %.4f" % (rank, recall), end=' ')
|
||||
print("R@%d: %.4f" % (rank, recall), end=" ")
|
||||
rank *= 10
|
||||
if endl:
|
||||
print()
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
|
||||
# https://stackoverflow.com/questions/7016056/python-logging-not-outputting-anything
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger('faiss.contrib.exhaustive_search')
|
||||
logger = logging.getLogger("faiss.contrib.exhaustive_search")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
from faiss.contrib import datasets
|
||||
@@ -18,10 +18,6 @@ ds = datasets.DatasetDeep1B(nb=int(1e9))
|
||||
|
||||
print("computing GT matches for", ds)
|
||||
|
||||
D, I = knn_ground_truth(
|
||||
ds.get_queries(),
|
||||
ds.database_iterator(bs=65536),
|
||||
k=100
|
||||
)
|
||||
D, I = knn_ground_truth(ds.get_queries(), ds.database_iterator(bs=65536), k=100)
|
||||
|
||||
vecs_io.ivecs_write("/tmp/tt.ivecs", I)
|
||||
|
||||
@@ -12,10 +12,10 @@ from faiss.contrib.factory_tools import get_code_size as unitsize
|
||||
|
||||
def dbsize_from_name(dbname):
|
||||
sufs = {
|
||||
'1B': 10**9,
|
||||
'100M': 10**8,
|
||||
'10M': 10**7,
|
||||
'1M': 10**6,
|
||||
"1B": 10**9,
|
||||
"100M": 10**8,
|
||||
"10M": 10**7,
|
||||
"1M": 10**6,
|
||||
}
|
||||
for s in sufs:
|
||||
if dbname.endswith(s):
|
||||
@@ -25,7 +25,7 @@ def dbsize_from_name(dbname):
|
||||
|
||||
|
||||
def keep_latest_stdout(fnames):
|
||||
fnames = [fname for fname in fnames if fname.endswith('.stdout')]
|
||||
fnames = [fname for fname in fnames if fname.endswith(".stdout")]
|
||||
fnames.sort()
|
||||
n = len(fnames)
|
||||
fnames2 = []
|
||||
@@ -42,7 +42,7 @@ def parse_result_file(fname):
|
||||
res = []
|
||||
keys = []
|
||||
stats = {}
|
||||
stats['run_version'] = fname[-8]
|
||||
stats["run_version"] = fname[-8]
|
||||
indexkey = None
|
||||
for l in open(fname):
|
||||
if l.startswith("srun:"):
|
||||
@@ -57,42 +57,42 @@ def parse_result_file(fname):
|
||||
stats["nq"] = int(fi[9])
|
||||
stats["nb"] = int(fi[11])
|
||||
stats["nt"] = int(fi[13])
|
||||
if l.startswith('index size on disk:'):
|
||||
stats['index_size'] = int(l.split()[-1])
|
||||
if l.startswith('current RSS:'):
|
||||
stats['RSS'] = int(l.split()[-1])
|
||||
if l.startswith('precomputed tables size:'):
|
||||
stats['tables_size'] = int(l.split()[-1])
|
||||
if l.startswith('Setting nb of threads to'):
|
||||
stats['n_threads'] = int(l.split()[-1])
|
||||
if l.startswith(' add in'):
|
||||
stats['add_time'] = float(l.split()[-2])
|
||||
if l.startswith("index size on disk:"):
|
||||
stats["index_size"] = int(l.split()[-1])
|
||||
if l.startswith("current RSS:"):
|
||||
stats["RSS"] = int(l.split()[-1])
|
||||
if l.startswith("precomputed tables size:"):
|
||||
stats["tables_size"] = int(l.split()[-1])
|
||||
if l.startswith("Setting nb of threads to"):
|
||||
stats["n_threads"] = int(l.split()[-1])
|
||||
if l.startswith(" add in"):
|
||||
stats["add_time"] = float(l.split()[-2])
|
||||
if l.startswith("vector code_size"):
|
||||
stats['code_size'] = float(l.split()[-1])
|
||||
if l.startswith('args:'):
|
||||
args = eval(l[l.find(' '):])
|
||||
stats["code_size"] = float(l.split()[-1])
|
||||
if l.startswith("args:"):
|
||||
args = eval(l[l.find(" ") :])
|
||||
indexkey = args.indexkey
|
||||
elif "time(ms/q)" in l:
|
||||
# result header
|
||||
if 'R@1 R@10 R@100' in l:
|
||||
if "R@1 R@10 R@100" in l:
|
||||
stats["measure"] = "recall"
|
||||
stats["ranks"] = [1, 10, 100]
|
||||
elif 'I@1 I@10 I@100' in l:
|
||||
elif "I@1 I@10 I@100" in l:
|
||||
stats["measure"] = "inter"
|
||||
stats["ranks"] = [1, 10, 100]
|
||||
elif 'inter@' in l:
|
||||
elif "inter@" in l:
|
||||
stats["measure"] = "inter"
|
||||
fi = l.split()
|
||||
if fi[1] == "inter@":
|
||||
rank = int(fi[2])
|
||||
else:
|
||||
rank = int(fi[1][len("inter@"):])
|
||||
rank = int(fi[1][len("inter@") :])
|
||||
stats["ranks"] = [rank]
|
||||
|
||||
else:
|
||||
assert False
|
||||
st = 1
|
||||
elif 'index size on disk:' in l:
|
||||
elif "index size on disk:" in l:
|
||||
stats["index_size"] = int(l.split()[-1])
|
||||
elif st == 1:
|
||||
st = 2
|
||||
@@ -105,12 +105,13 @@ def parse_result_file(fname):
|
||||
res.append([float(x) for x in fi[1:]])
|
||||
return indexkey, np.array(res), keys, stats
|
||||
|
||||
|
||||
# the directory used in run_on_cluster.bash
|
||||
basedir = "/checkpoint/matthijs/bench_all_ivf/"
|
||||
logdir = basedir + 'logs/'
|
||||
logdir = basedir + "logs/"
|
||||
|
||||
|
||||
def collect_results_for(db='deep1M', prefix="autotune."):
|
||||
def collect_results_for(db="deep1M", prefix="autotune."):
|
||||
# run parsing
|
||||
allres = {}
|
||||
allstats = {}
|
||||
@@ -124,10 +125,10 @@ def collect_results_for(db='deep1M', prefix="autotune."):
|
||||
|
||||
for fname in fnames:
|
||||
if not (
|
||||
'db' + db in fname and
|
||||
fname.startswith(prefix) and
|
||||
fname.endswith('.stdout')
|
||||
):
|
||||
"db" + db in fname
|
||||
and fname.startswith(prefix)
|
||||
and fname.endswith(".stdout")
|
||||
):
|
||||
continue
|
||||
print("parse", fname, end=" ", flush=True)
|
||||
try:
|
||||
@@ -144,7 +145,7 @@ def collect_results_for(db='deep1M', prefix="autotune."):
|
||||
missing.append(fname)
|
||||
else:
|
||||
if indexkey in allres:
|
||||
if allstats[indexkey]['run_version'] > stats['run_version']:
|
||||
if allstats[indexkey]["run_version"] > stats["run_version"]:
|
||||
# don't use this run
|
||||
continue
|
||||
|
||||
@@ -160,12 +161,7 @@ def extract_pareto_optimal(allres, keys, recall_idx=0, times_idx=3):
|
||||
v = allres[k]
|
||||
perf = v[:, recall_idx]
|
||||
times = v[:, times_idx]
|
||||
bigtab.append(
|
||||
np.vstack((
|
||||
np.ones(times.size) * i,
|
||||
perf, times
|
||||
))
|
||||
)
|
||||
bigtab.append(np.vstack((np.ones(times.size) * i, perf, times)))
|
||||
if bigtab == []:
|
||||
return [], np.zeros((3, 0))
|
||||
|
||||
@@ -175,10 +171,9 @@ def extract_pareto_optimal(allres, keys, recall_idx=0, times_idx=3):
|
||||
perm = np.argsort(bigtab[1, :])
|
||||
bigtab_sorted = bigtab[:, perm]
|
||||
best_times = np.minimum.accumulate(bigtab_sorted[2, ::-1])[::-1]
|
||||
selection, = np.where(bigtab_sorted[2, :] == best_times)
|
||||
(selection,) = np.where(bigtab_sorted[2, :] == best_times)
|
||||
selected_keys = [
|
||||
keys[i] for i in
|
||||
np.unique(bigtab_sorted[0, selection].astype(int))
|
||||
keys[i] for i in np.unique(bigtab_sorted[0, selection].astype(int))
|
||||
]
|
||||
ops = bigtab_sorted[:, selection]
|
||||
|
||||
@@ -186,8 +181,13 @@ def extract_pareto_optimal(allres, keys, recall_idx=0, times_idx=3):
|
||||
|
||||
|
||||
def plot_subset(
|
||||
allres, allstats, selected_methods, recall_idx, times_idx=3,
|
||||
report=["overhead", "build time"]):
|
||||
allres,
|
||||
allstats,
|
||||
selected_methods,
|
||||
recall_idx,
|
||||
times_idx=3,
|
||||
report=["overhead", "build time"],
|
||||
):
|
||||
|
||||
# important methods
|
||||
for k in selected_methods:
|
||||
@@ -197,20 +197,20 @@ def plot_subset(
|
||||
d = stats["d"]
|
||||
dbsize = stats["nb"]
|
||||
if "index_size" in stats and "tables_size" in stats:
|
||||
tot_size = stats['index_size'] + stats['tables_size']
|
||||
tot_size = stats["index_size"] + stats["tables_size"]
|
||||
else:
|
||||
tot_size = -1
|
||||
id_size = 8 # 64 bit
|
||||
id_size = 8 # 64 bit
|
||||
|
||||
addt = ''
|
||||
if 'add_time' in stats:
|
||||
add_time = stats['add_time']
|
||||
addt = ""
|
||||
if "add_time" in stats:
|
||||
add_time = stats["add_time"]
|
||||
if add_time > 7200:
|
||||
add_min = add_time / 60
|
||||
addt = ', %dh%02d' % (add_min / 60, add_min % 60)
|
||||
addt = ", %dh%02d" % (add_min / 60, add_min % 60)
|
||||
else:
|
||||
add_sec = int(add_time)
|
||||
addt = ', %dm%02d' % (add_sec / 60, add_sec % 60)
|
||||
addt = ", %dm%02d" % (add_sec / 60, add_sec % 60)
|
||||
|
||||
code_size = unitsize(d, k)
|
||||
|
||||
@@ -222,33 +222,37 @@ def plot_subset(
|
||||
tight_size = (code_size + id_size) * dbsize
|
||||
|
||||
if tot_size < 0 or "overhead" not in report:
|
||||
pass # don't know what the index size is
|
||||
pass # don't know what the index size is
|
||||
elif tot_size > 10 * tight_size:
|
||||
label += " overhead x%.1f" % (tot_size / tight_size)
|
||||
else:
|
||||
label += " overhead+%.1f%%" % (
|
||||
tot_size / tight_size * 100 - 100)
|
||||
label += " overhead+%.1f%%" % (tot_size / tight_size * 100 - 100)
|
||||
|
||||
if "build time" in report:
|
||||
label += " " + addt
|
||||
|
||||
linestyle = (':' if 'Refine' in k or 'RFlat' in k else
|
||||
'-.' if 'SQ' in k else
|
||||
'-' if '4fs' in k else
|
||||
'-')
|
||||
linestyle = (
|
||||
":"
|
||||
if "Refine" in k or "RFlat" in k
|
||||
else "-." if "SQ" in k else "-" if "4fs" in k else "-"
|
||||
)
|
||||
print(k, linestyle)
|
||||
pyplot.semilogy(v[:, recall_idx], 1000 / v[:, times_idx], label=label,
|
||||
linestyle=linestyle,
|
||||
marker='o' if '4fs' in k else '+')
|
||||
pyplot.semilogy(
|
||||
v[:, recall_idx],
|
||||
1000 / v[:, times_idx],
|
||||
label=label,
|
||||
linestyle=linestyle,
|
||||
marker="o" if "4fs" in k else "+",
|
||||
)
|
||||
|
||||
recall_rank = stats["ranks"][recall_idx]
|
||||
if stats["measure"] == "recall":
|
||||
pyplot.xlabel('1-recall at %d' % recall_rank)
|
||||
pyplot.xlabel("1-recall at %d" % recall_rank)
|
||||
elif stats["measure"] == "inter":
|
||||
pyplot.xlabel('inter @ %d' % recall_rank)
|
||||
pyplot.xlabel("inter @ %d" % recall_rank)
|
||||
else:
|
||||
assert False
|
||||
pyplot.ylabel('QPS (%d threads)' % stats["n_threads"])
|
||||
pyplot.ylabel("QPS (%d threads)" % stats["n_threads"])
|
||||
|
||||
|
||||
def plot_tradeoffs(db, allres, allstats, code_size, recall_rank):
|
||||
@@ -275,66 +279,76 @@ def plot_tradeoffs(db, allres, allstats, code_size, recall_rank):
|
||||
|
||||
for k in sorted(allres):
|
||||
v = allres[k]
|
||||
if v.ndim != 2: continue
|
||||
if v.ndim != 2:
|
||||
continue
|
||||
us = unitsize(d, k)
|
||||
if not code_size[0] <= us <= code_size[1]: continue
|
||||
if not code_size[0] <= us <= code_size[1]:
|
||||
continue
|
||||
names_maxperf.append((v[-1, recall_idx], k))
|
||||
|
||||
# sort from lowest to highest topline accuracy
|
||||
names_maxperf.sort()
|
||||
names = [name for mp, name in names_maxperf]
|
||||
|
||||
selected_methods, optimal_points = \
|
||||
extract_pareto_optimal(allres, names, recall_idx, times_idx)
|
||||
selected_methods, optimal_points = extract_pareto_optimal(
|
||||
allres, names, recall_idx, times_idx
|
||||
)
|
||||
|
||||
not_selected = list(set(names) - set(selected_methods))
|
||||
|
||||
print("methods without an optimal OP: ", not_selected)
|
||||
|
||||
pyplot.title('database ' + db + ' ' + code_size_name)
|
||||
pyplot.title("database " + db + " " + code_size_name)
|
||||
|
||||
# grayed out lines
|
||||
|
||||
for k in not_selected:
|
||||
v = allres[k]
|
||||
if v.ndim != 2: continue
|
||||
if v.ndim != 2:
|
||||
continue
|
||||
us = unitsize(d, k)
|
||||
if not code_size[0] <= us <= code_size[1]: continue
|
||||
if not code_size[0] <= us <= code_size[1]:
|
||||
continue
|
||||
|
||||
linestyle = (':' if 'PQ' in k else
|
||||
'-.' if 'SQ4' in k else
|
||||
'--' if 'SQ8' in k else '-')
|
||||
linestyle = (
|
||||
":"
|
||||
if "PQ" in k
|
||||
else "-." if "SQ4" in k else "--" if "SQ8" in k else "-"
|
||||
)
|
||||
|
||||
pyplot.semilogy(v[:, recall_idx], 1000 / v[:, times_idx], label=None,
|
||||
linestyle=linestyle,
|
||||
marker='o' if 'HNSW' in k else '+',
|
||||
color='#cccccc', linewidth=0.2)
|
||||
pyplot.semilogy(
|
||||
v[:, recall_idx],
|
||||
1000 / v[:, times_idx],
|
||||
label=None,
|
||||
linestyle=linestyle,
|
||||
marker="o" if "HNSW" in k else "+",
|
||||
color="#cccccc",
|
||||
linewidth=0.2,
|
||||
)
|
||||
|
||||
plot_subset(allres, allstats, selected_methods, recall_idx, times_idx)
|
||||
|
||||
|
||||
if len(not_selected) == 0:
|
||||
om = ''
|
||||
om = ""
|
||||
else:
|
||||
om = '\nomitted:'
|
||||
om = "\nomitted:"
|
||||
nc = len(om)
|
||||
for m in not_selected:
|
||||
if nc > 80:
|
||||
om += '\n'
|
||||
om += "\n"
|
||||
nc = 0
|
||||
om += ' ' + m
|
||||
om += " " + m
|
||||
nc += len(m) + 1
|
||||
|
||||
# pyplot.semilogy(optimal_points[1, :], optimal_points[2, :], marker="s")
|
||||
# print(optimal_points[0, :])
|
||||
pyplot.xlabel('1-recall at %d %s' % (recall_rank, om) )
|
||||
pyplot.ylabel('QPS (%d threads)' % n_threads)
|
||||
pyplot.xlabel("1-recall at %d %s" % (recall_rank, om))
|
||||
pyplot.ylabel("QPS (%d threads)" % n_threads)
|
||||
pyplot.legend()
|
||||
pyplot.grid()
|
||||
return selected_methods, not_selected
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__xx":
|
||||
# tests on centroids indexing (v1)
|
||||
|
||||
@@ -342,16 +356,17 @@ if __name__ == "__main__xx":
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
i = 1
|
||||
for ncent in 65536, 262144, 1048576, 4194304:
|
||||
db = f'deep_centroids_{ncent}.k{k}.'
|
||||
allres, allstats = collect_results_for(
|
||||
db=db, prefix="cent_index.")
|
||||
db = f"deep_centroids_{ncent}.k{k}."
|
||||
allres, allstats = collect_results_for(db=db, prefix="cent_index.")
|
||||
|
||||
pyplot.subplot(2, 2, i)
|
||||
plot_subset(
|
||||
allres, allstats, list(allres.keys()),
|
||||
allres,
|
||||
allstats,
|
||||
list(allres.keys()),
|
||||
recall_idx=0,
|
||||
times_idx=1,
|
||||
report=["code_size"]
|
||||
report=["code_size"],
|
||||
)
|
||||
i += 1
|
||||
pyplot.title(f"{ncent} centroids")
|
||||
@@ -359,7 +374,7 @@ if __name__ == "__main__xx":
|
||||
pyplot.xlim([0.95, 1])
|
||||
pyplot.grid()
|
||||
|
||||
pyplot.savefig('figs/deep1B_centroids_k%d.png' % k)
|
||||
pyplot.savefig("figs/deep1B_centroids_k%d.png" % k)
|
||||
|
||||
|
||||
if __name__ == "__main__xx":
|
||||
@@ -367,18 +382,18 @@ if __name__ == "__main__xx":
|
||||
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
|
||||
i=1
|
||||
i = 1
|
||||
for ncent in 65536, 262144, 1048576, 4194304:
|
||||
|
||||
xyd = defaultdict(list)
|
||||
|
||||
for k in 1, 4, 8, 16, 32, 64, 128, 256:
|
||||
|
||||
db = f'deep_centroids_{ncent}.k{k}.'
|
||||
db = f"deep_centroids_{ncent}.k{k}."
|
||||
allres, allstats = collect_results_for(db=db, prefix="cent_index.")
|
||||
|
||||
for indexkey, res in allres.items():
|
||||
idx, = np.where(res[:, 0] >= 0.99)
|
||||
(idx,) = np.where(res[:, 0] >= 0.99)
|
||||
if idx.size > 0:
|
||||
xyd[indexkey].append((k, 1000 / res[idx[0], 1]))
|
||||
|
||||
@@ -386,43 +401,44 @@ if __name__ == "__main__xx":
|
||||
i += 1
|
||||
for indexkey, xy in xyd.items():
|
||||
xy = np.array(xy)
|
||||
pyplot.loglog(xy[:, 0], xy[:, 1], 'o-', label=indexkey)
|
||||
pyplot.loglog(xy[:, 0], xy[:, 1], "o-", label=indexkey)
|
||||
|
||||
pyplot.title(f"{ncent} centroids")
|
||||
pyplot.xlabel("k")
|
||||
xt = 2**np.arange(9)
|
||||
xt = 2 ** np.arange(9)
|
||||
pyplot.xticks(xt, ["%d" % x for x in xt])
|
||||
pyplot.ylabel("QPS (32 threads)")
|
||||
pyplot.legend()
|
||||
pyplot.grid()
|
||||
|
||||
pyplot.savefig('../plots/deep1B_centroids_min99.png')
|
||||
|
||||
|
||||
|
||||
pyplot.savefig("../plots/deep1B_centroids_min99.png")
|
||||
|
||||
|
||||
if __name__ == "__main__xx":
|
||||
# main indexing plots
|
||||
|
||||
i = 0
|
||||
for db in 'bigann10M', 'deep10M', 'bigann100M', 'deep100M', 'deep1B', 'bigann1B':
|
||||
allres, allstats = collect_results_for(
|
||||
db=db, prefix="autotune.")
|
||||
for db in (
|
||||
"bigann10M",
|
||||
"deep10M",
|
||||
"bigann100M",
|
||||
"deep100M",
|
||||
"deep1B",
|
||||
"bigann1B",
|
||||
):
|
||||
allres, allstats = collect_results_for(db=db, prefix="autotune.")
|
||||
|
||||
for cs in 8, 16, 32, 64:
|
||||
pyplot.figure(i)
|
||||
i += 1
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
|
||||
cs_range = (
|
||||
(0, 8) if cs == 8 else (cs // 2 + 1, cs)
|
||||
)
|
||||
cs_range = (0, 8) if cs == 8 else (cs // 2 + 1, cs)
|
||||
|
||||
plot_tradeoffs(
|
||||
db, allres, allstats, code_size=cs_range, recall_rank=1)
|
||||
pyplot.savefig('../plots/tradeoffs_%s_cs%d_r1.png' % (
|
||||
db, cs))
|
||||
db, allres, allstats, code_size=cs_range, recall_rank=1
|
||||
)
|
||||
pyplot.savefig("../plots/tradeoffs_%s_cs%d_r1.png" % (db, cs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -434,7 +450,7 @@ if __name__ == "__main__":
|
||||
i += 1
|
||||
allres, allstats = collect_results_for(db=db, prefix="autotune.")
|
||||
plot_tradeoffs(db, allres, allstats, code_size=0, recall_rank=1)
|
||||
pyplot.savefig('../plots/1M_tradeoffs_' + db + ".png")
|
||||
pyplot.savefig("../plots/1M_tradeoffs_" + db + ".png")
|
||||
|
||||
for db in "sift1M", "deep1M":
|
||||
allres, allstats = collect_results_for(db=db, prefix="autotune.")
|
||||
@@ -442,18 +458,19 @@ if __name__ == "__main__":
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
i += 1
|
||||
plot_tradeoffs(db, allres, allstats, code_size=(0, 64), recall_rank=1)
|
||||
pyplot.savefig('../plots/1M_tradeoffs_' + db + "_small.png")
|
||||
pyplot.savefig("../plots/1M_tradeoffs_" + db + "_small.png")
|
||||
|
||||
pyplot.figure(i)
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
i += 1
|
||||
plot_tradeoffs(db, allres, allstats, code_size=(65, 10000), recall_rank=1)
|
||||
pyplot.savefig('../plots/1M_tradeoffs_' + db + "_large.png")
|
||||
|
||||
plot_tradeoffs(
|
||||
db, allres, allstats, code_size=(65, 10000), recall_rank=1
|
||||
)
|
||||
pyplot.savefig("../plots/1M_tradeoffs_" + db + "_large.png")
|
||||
|
||||
|
||||
if __name__ == "__main__xx":
|
||||
db = 'sift1M'
|
||||
db = "sift1M"
|
||||
allres, allstats = collect_results_for(db=db, prefix="autotune.")
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
|
||||
@@ -463,7 +480,7 @@ if __name__ == "__main__xx":
|
||||
"IVF1024,PQ64x4fs",
|
||||
"IVF1024,PQ64x4fsr",
|
||||
"IVF1024,SQ4",
|
||||
"IVF1024,SQ8"
|
||||
"IVF1024,SQ8",
|
||||
]
|
||||
|
||||
plot_subset(allres, allstats, keys, recall_idx=0, report=["code_size"])
|
||||
@@ -474,7 +491,7 @@ if __name__ == "__main__xx":
|
||||
pyplot.ylabel("QPS (32 threads)")
|
||||
pyplot.grid()
|
||||
|
||||
pyplot.savefig('../plots/ivf1024_variants.png')
|
||||
pyplot.savefig("../plots/ivf1024_variants.png")
|
||||
|
||||
pyplot.figure(2)
|
||||
pyplot.gcf().set_size_inches(15, 10)
|
||||
@@ -496,4 +513,4 @@ if __name__ == "__main__xx":
|
||||
pyplot.ylabel("QPS (32 threads)")
|
||||
pyplot.grid()
|
||||
|
||||
pyplot.savefig('../plots/ivf1024_rerank.png')
|
||||
pyplot.savefig("../plots/ivf1024_rerank.png")
|
||||
|
||||
@@ -20,17 +20,17 @@ def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
group = parser.add_argument_group('dataset options')
|
||||
aa('--dim', type=int, default=64)
|
||||
aa('--size', default="S")
|
||||
group = parser.add_argument_group("dataset options")
|
||||
aa("--dim", type=int, default=64)
|
||||
aa("--size", default="S")
|
||||
|
||||
group = parser.add_argument_group('index options')
|
||||
aa('--nlist', type=int, default=100)
|
||||
aa('--factory_string', default="", help="overrides nlist")
|
||||
aa('--k', type=int, default=10)
|
||||
aa('--nprobe', type=int, default=5)
|
||||
aa('--nt', type=int, default=-1, help="nb search threads")
|
||||
aa('--method', default="pairwise_distances", help="")
|
||||
group = parser.add_argument_group("index options")
|
||||
aa("--nlist", type=int, default=100)
|
||||
aa("--factory_string", default="", help="overrides nlist")
|
||||
aa("--k", type=int, default=10)
|
||||
aa("--nprobe", type=int, default=5)
|
||||
aa("--nt", type=int, default=-1, help="nb search threads")
|
||||
aa("--method", default="pairwise_distances", help="")
|
||||
|
||||
args = parser.parse_args()
|
||||
print("args:", args)
|
||||
@@ -98,8 +98,7 @@ t_ref = toc()
|
||||
|
||||
tic("block search")
|
||||
Dnew, Inew = big_batch_search(
|
||||
index, ds.get_queries(),
|
||||
k, method=args.method, verbose=10
|
||||
index, ds.get_queries(), k, method=args.method, verbose=10
|
||||
)
|
||||
t_tot = toc()
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ except ImportError:
|
||||
from faiss.contrib.datasets import DatasetSIFT1M, DatasetGIST1M
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--dataset", default="gist1m", choices=["sift1m", "gist1m"]
|
||||
)
|
||||
parser.add_argument("--dataset", default="gist1m", choices=["sift1m", "gist1m"])
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dataset == "sift1m":
|
||||
@@ -91,15 +89,15 @@ for name in names:
|
||||
qps_values.append(qps)
|
||||
|
||||
x = np.arange(len(names))
|
||||
plt.bar(x, qps_values, color=['#1f77b4', '#ff7f0e'])
|
||||
plt.bar(x, qps_values, color=["#1f77b4", "#ff7f0e"])
|
||||
speedup = qps_values[1] / qps_values[0]
|
||||
ax = plt.gca()
|
||||
ax.text(
|
||||
x[1],
|
||||
qps_values[1] * 1.01,
|
||||
f"{speedup:.2f}x",
|
||||
ha="center",
|
||||
va="bottom",
|
||||
x[1],
|
||||
qps_values[1] * 1.01,
|
||||
f"{speedup:.2f}x",
|
||||
ha="center",
|
||||
va="bottom",
|
||||
)
|
||||
plt.xticks(x, labels, rotation=0)
|
||||
plt.ylabel("QPS")
|
||||
|
||||
@@ -18,15 +18,16 @@ parser = argparse.ArgumentParser()
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('dataset options')
|
||||
aa('--dim', type=int, default=64)
|
||||
aa('--nb', type=int, default=int(1e6))
|
||||
aa('--subset_len', type=int, default=int(1e5))
|
||||
aa('--key', default='IVF1000,Flat')
|
||||
aa('--nprobe', type=int, default=640)
|
||||
aa('--no_intcallback', default=False, action='store_true')
|
||||
aa('--twostage', default=False, action='store_true')
|
||||
aa('--nt', type=int, default=-1)
|
||||
|
||||
group = parser.add_argument_group("dataset options")
|
||||
aa("--dim", type=int, default=64)
|
||||
aa("--nb", type=int, default=int(1e6))
|
||||
aa("--subset_len", type=int, default=int(1e5))
|
||||
aa("--key", default="IVF1000,Flat")
|
||||
aa("--nprobe", type=int, default=640)
|
||||
aa("--no_intcallback", default=False, action="store_true")
|
||||
aa("--twostage", default=False, action="store_true")
|
||||
aa("--nt", type=int, default=-1)
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -41,9 +42,9 @@ subset_len = args.subset_len
|
||||
|
||||
|
||||
np.random.seed(1234) # make reproducible
|
||||
xb = np.random.random((nb, d)).astype('float32')
|
||||
xq = np.random.random((nq, d)).astype('float32')
|
||||
xt = np.random.random((nt, d)).astype('float32')
|
||||
xb = np.random.random((nb, d)).astype("float32")
|
||||
xq = np.random.random((nq, d)).astype("float32")
|
||||
xt = np.random.random((nt, d)).astype("float32")
|
||||
k = 100
|
||||
|
||||
if args.no_intcallback:
|
||||
@@ -54,41 +55,41 @@ if args.nt != -1:
|
||||
|
||||
nprobe = args.nprobe
|
||||
key = args.key
|
||||
#key = 'IVF1000,Flat'
|
||||
# key = 'IVF1000,Flat'
|
||||
# key = 'IVF1000,PQ64'
|
||||
# key = 'IVF100_HNSW32,PQ64'
|
||||
|
||||
# faiss.omp_set_num_threads(1)
|
||||
|
||||
pf = 'dim%d_' % d
|
||||
pf = "dim%d_" % d
|
||||
if d == 64:
|
||||
pf = ''
|
||||
pf = ""
|
||||
|
||||
basename = '/tmp/base%s%s.index' % (pf, key)
|
||||
basename = "/tmp/base%s%s.index" % (pf, key)
|
||||
|
||||
if os.path.exists(basename):
|
||||
print('load', basename)
|
||||
print("load", basename)
|
||||
index_1 = faiss.read_index(basename)
|
||||
else:
|
||||
print('train + write', basename)
|
||||
print("train + write", basename)
|
||||
index_1 = faiss.index_factory(d, key)
|
||||
index_1.train(xt)
|
||||
faiss.write_index(index_1, basename)
|
||||
|
||||
print('add')
|
||||
print("add")
|
||||
index_1.add(xb)
|
||||
|
||||
print('set nprobe=', nprobe)
|
||||
faiss.ParameterSpace().set_index_parameter(index_1, 'nprobe', nprobe)
|
||||
print("set nprobe=", nprobe)
|
||||
faiss.ParameterSpace().set_index_parameter(index_1, "nprobe", nprobe)
|
||||
|
||||
|
||||
class ResultHeap:
|
||||
""" Combine query results from a sliced dataset """
|
||||
"""Combine query results from a sliced dataset"""
|
||||
|
||||
def __init__(self, nq, k):
|
||||
" nq: number of query vectors, k: number of results per query "
|
||||
self.I = np.zeros((nq, k), dtype='int64')
|
||||
self.D = np.zeros((nq, k), dtype='float32')
|
||||
"nq: number of query vectors, k: number of results per query"
|
||||
self.I = np.zeros((nq, k), dtype="int64")
|
||||
self.D = np.zeros((nq, k), dtype="float32")
|
||||
self.nq, self.k = nq, k
|
||||
heaps = faiss.float_maxheap_array_t()
|
||||
heaps.k = k
|
||||
@@ -103,30 +104,37 @@ class ResultHeap:
|
||||
assert I.shape == (self.nq, self.k)
|
||||
I += i0
|
||||
self.heaps.addn_with_ids(
|
||||
self.k, faiss.swig_ptr(D),
|
||||
faiss.swig_ptr(I), self.k)
|
||||
self.k, faiss.swig_ptr(D), faiss.swig_ptr(I), self.k
|
||||
)
|
||||
|
||||
def finalize(self):
|
||||
self.heaps.reorder()
|
||||
|
||||
|
||||
stats = faiss.cvar.indexIVF_stats
|
||||
stats.reset()
|
||||
|
||||
print('index size', index_1.ntotal,
|
||||
'imbalance', index_1.invlists.imbalance_factor())
|
||||
print(
|
||||
"index size",
|
||||
index_1.ntotal,
|
||||
"imbalance",
|
||||
index_1.invlists.imbalance_factor(),
|
||||
)
|
||||
start = time.time()
|
||||
Dref, Iref = index_1.search(xq, k)
|
||||
print('time of searching: %.3f s = %.3f + %.3f ms' % (
|
||||
time.time() - start, stats.quantization_time, stats.search_time))
|
||||
print(
|
||||
"time of searching: %.3f s = %.3f + %.3f ms"
|
||||
% (time.time() - start, stats.quantization_time, stats.search_time)
|
||||
)
|
||||
|
||||
indexes = {}
|
||||
if args.twostage:
|
||||
|
||||
for i in range(0, nb, subset_len):
|
||||
index = faiss.read_index(basename)
|
||||
faiss.ParameterSpace().set_index_parameter(index, 'nprobe', nprobe)
|
||||
print("add %d:%d" %(i, i+subset_len))
|
||||
index.add(xb[i:i + subset_len])
|
||||
faiss.ParameterSpace().set_index_parameter(index, "nprobe", nprobe)
|
||||
print("add %d:%d" % (i, i + subset_len))
|
||||
index.add(xb[i : i + subset_len])
|
||||
indexes[i] = index
|
||||
|
||||
rh = ResultHeap(nq, k)
|
||||
@@ -134,9 +142,9 @@ sum_time = tq = ts = 0
|
||||
for i in range(0, nb, subset_len):
|
||||
if not args.twostage:
|
||||
index = faiss.read_index(basename)
|
||||
faiss.ParameterSpace().set_index_parameter(index, 'nprobe', nprobe)
|
||||
print("add %d:%d" %(i, i+subset_len))
|
||||
index.add(xb[i:i + subset_len])
|
||||
faiss.ParameterSpace().set_index_parameter(index, "nprobe", nprobe)
|
||||
print("add %d:%d" % (i, i + subset_len))
|
||||
index.add(xb[i : i + subset_len])
|
||||
else:
|
||||
index = indexes[i]
|
||||
|
||||
@@ -148,9 +156,10 @@ for i in range(0, nb, subset_len):
|
||||
ts += stats.search_time
|
||||
rh.add_batch_result(Di, Ii, i)
|
||||
|
||||
print('time of searching separately: %.3f s = %.3f + %.3f ms' %
|
||||
(sum_time, tq, ts))
|
||||
print(
|
||||
"time of searching separately: %.3f s = %.3f + %.3f ms" % (sum_time, tq, ts)
|
||||
)
|
||||
|
||||
rh.finalize()
|
||||
|
||||
print('diffs: %d / %d' % ((Iref != rh.I).sum(), Iref.size))
|
||||
print("diffs: %d / %d" % ((Iref != rh.I).sum(), Iref.size))
|
||||
|
||||
@@ -239,7 +239,8 @@ class TrainOperator(IndexOperator):
|
||||
|
||||
if codec_desc.factory is not None:
|
||||
assert (
|
||||
codec_desc.factory == "Flat" or codec_desc.training_vectors is not None
|
||||
codec_desc.factory == "Flat"
|
||||
or codec_desc.training_vectors is not None
|
||||
)
|
||||
index = IndexFromFactory(
|
||||
num_threads=self.num_threads,
|
||||
@@ -257,7 +258,10 @@ class TrainOperator(IndexOperator):
|
||||
assert codec_desc.is_trained()
|
||||
|
||||
def train_one(
|
||||
self, codec_desc: CodecDescriptor, results: Dict[str, Any], dry_run=False
|
||||
self,
|
||||
codec_desc: CodecDescriptor,
|
||||
results: Dict[str, Any],
|
||||
dry_run=False,
|
||||
):
|
||||
faiss.omp_set_num_threads(codec_desc.num_threads)
|
||||
self.build_index_wrapper(codec_desc)
|
||||
@@ -390,7 +394,9 @@ class SearchOperator(IndexOperator):
|
||||
|
||||
knn_desc.index.get_index()
|
||||
|
||||
def range_search_reference(self, index, parameters, range_metric, query_dataset):
|
||||
def range_search_reference(
|
||||
self, index, parameters, range_metric, query_dataset
|
||||
):
|
||||
logger.info("range_search_reference: begin")
|
||||
if isinstance(range_metric, list):
|
||||
assert len(range_metric) > 0
|
||||
@@ -428,7 +434,9 @@ class SearchOperator(IndexOperator):
|
||||
coefficients_training_data,
|
||||
)
|
||||
|
||||
def estimate_range(self, index, parameters, range_scoring_radius, query_dataset):
|
||||
def estimate_range(
|
||||
self, index, parameters, range_scoring_radius, query_dataset
|
||||
):
|
||||
D, I, R, P, _ = index.knn_search(
|
||||
False,
|
||||
parameters,
|
||||
@@ -477,7 +485,9 @@ class SearchOperator(IndexOperator):
|
||||
return None, None, None, None, None, requires
|
||||
if range_search_metric_function is not None:
|
||||
range_search_metric = range_search_metric_function(R)
|
||||
range_search_pr = range_search_pr_curve(D, range_search_metric, gt_rsm)
|
||||
range_search_pr = range_search_pr_curve(
|
||||
D, range_search_metric, gt_rsm
|
||||
)
|
||||
range_score_sum = np.sum(range_search_metric).item()
|
||||
P |= {
|
||||
"range_score_sum": range_score_sum,
|
||||
@@ -716,8 +726,8 @@ class SearchOperator(IndexOperator):
|
||||
assert requires is None
|
||||
|
||||
if (
|
||||
knn_desc.range_ref_index_desc is None or
|
||||
not knn_desc.index.supports_range_search()
|
||||
knn_desc.range_ref_index_desc is None
|
||||
or not knn_desc.index.supports_range_search()
|
||||
):
|
||||
return results, None
|
||||
|
||||
@@ -778,15 +788,17 @@ class SearchOperator(IndexOperator):
|
||||
return results, None
|
||||
|
||||
def search(
|
||||
self,
|
||||
results: Dict[str, Any],
|
||||
dry_run: bool = False,):
|
||||
self,
|
||||
results: Dict[str, Any],
|
||||
dry_run: bool = False,
|
||||
):
|
||||
for knn_desc in self.knn_descs:
|
||||
results, requires = self.search_one(
|
||||
knn_desc=knn_desc,
|
||||
results=results,
|
||||
dry_run=dry_run,
|
||||
range=self.range)
|
||||
range=self.range,
|
||||
)
|
||||
if dry_run:
|
||||
if requires is None:
|
||||
continue
|
||||
@@ -901,24 +913,29 @@ class ExecutionOperator:
|
||||
|
||||
def create_range_ref_knn(self, knn_desc):
|
||||
if (
|
||||
knn_desc.range_ref_index_desc is None or
|
||||
not knn_desc.index.supports_range_search()
|
||||
knn_desc.range_ref_index_desc is None
|
||||
or not knn_desc.index.supports_range_search()
|
||||
):
|
||||
return
|
||||
|
||||
if knn_desc.range_ref_index_desc is not None:
|
||||
ref_index_desc = (
|
||||
self.search_op.get_desc(knn_desc.range_ref_index_desc)
|
||||
ref_index_desc = self.search_op.get_desc(
|
||||
knn_desc.range_ref_index_desc
|
||||
)
|
||||
if ref_index_desc is None:
|
||||
raise ValueError(f"Unknown range index {knn_desc.range_ref_index_desc}")
|
||||
raise ValueError(
|
||||
f"Unknown range index {knn_desc.range_ref_index_desc}"
|
||||
)
|
||||
if ref_index_desc.range_metrics is None:
|
||||
raise ValueError(
|
||||
f"Range index {knn_desc.get_name()} has no radius_score"
|
||||
)
|
||||
results["metrics"] = {}
|
||||
self.build_index_wrapper(ref_index_desc)
|
||||
for metric_key, range_metric in ref_index_desc.range_metrics.items():
|
||||
for (
|
||||
metric_key,
|
||||
range_metric,
|
||||
) in ref_index_desc.range_metrics.items():
|
||||
(
|
||||
knn_desc.gt_radius,
|
||||
range_search_metric_function,
|
||||
@@ -962,8 +979,8 @@ class ExecutionOperator:
|
||||
def execute(self, results: Dict[str, Any], dry_run: bool = False):
|
||||
faiss.omp_set_num_threads(self.num_threads)
|
||||
if self.train_op is not None:
|
||||
results, requires = (
|
||||
self.train_op.train(results=results, dry_run=dry_run)
|
||||
results, requires = self.train_op.train(
|
||||
results=results, dry_run=dry_run
|
||||
)
|
||||
if dry_run and requires:
|
||||
return results, requires
|
||||
@@ -975,8 +992,8 @@ class ExecutionOperator:
|
||||
if not dry_run and self.compute_gt:
|
||||
self.prepare_gt_or_range_knn(results)
|
||||
|
||||
results, requires = (
|
||||
self.search_op.search(results=results, dry_run=dry_run)
|
||||
results, requires = self.search_op.search(
|
||||
results=results, dry_run=dry_run
|
||||
)
|
||||
if dry_run and requires:
|
||||
return results, requires
|
||||
@@ -1025,7 +1042,13 @@ class Benchmark:
|
||||
raise ValueError("Failed to determine dimension of dataset")
|
||||
|
||||
def create_descriptors(
|
||||
self, ci_desc: IndexDescriptorClassic, train, build, knn, reconstruct, range
|
||||
self,
|
||||
ci_desc: IndexDescriptorClassic,
|
||||
train,
|
||||
build,
|
||||
knn,
|
||||
reconstruct,
|
||||
range,
|
||||
):
|
||||
codec_desc = None
|
||||
index_desc = None
|
||||
@@ -1126,7 +1149,9 @@ class Benchmark:
|
||||
database_vectors=self.database_vectors,
|
||||
query_vectors=self.query_vectors,
|
||||
# index_descs=[self.get_flat_desc("Flat"), index_desc],
|
||||
index_descs=[index_desc], # Should automatically find flat descriptors
|
||||
index_descs=[
|
||||
index_desc
|
||||
], # Should automatically find flat descriptors
|
||||
range_ref_index_desc=self.range_ref_index_desc,
|
||||
k=self.k,
|
||||
distance_metric=self.distance_metric,
|
||||
|
||||
@@ -142,9 +142,9 @@ class DatasetDescriptor:
|
||||
assert self.tablename is not None
|
||||
filename += self.tablename
|
||||
if self.partitions is not None:
|
||||
filename += "_" + "_".join(
|
||||
self.partitions
|
||||
).replace("=", "_").replace("/", "_")
|
||||
filename += "_" + "_".join(self.partitions).replace(
|
||||
"=", "_"
|
||||
).replace("/", "_")
|
||||
if self.num_vectors is not None:
|
||||
filename += f"_{self.num_vectors}"
|
||||
if self.filename_suffix is not None:
|
||||
@@ -278,8 +278,12 @@ class CodecDescriptor(IndexBaseDescriptor):
|
||||
name += f"d_{self.d}.{self.metric.upper()}."
|
||||
if self.factory != "Flat":
|
||||
assert self.training_vectors is not None
|
||||
name += self.training_vectors.get_filename(CodecDescriptor.FILENAME_PREFIX)
|
||||
name += IndexBaseDescriptor.param_dict_list_to_name(self.construction_params)
|
||||
name += self.training_vectors.get_filename(
|
||||
CodecDescriptor.FILENAME_PREFIX
|
||||
)
|
||||
name += IndexBaseDescriptor.param_dict_list_to_name(
|
||||
self.construction_params
|
||||
)
|
||||
return name
|
||||
|
||||
def name_from_path(self):
|
||||
@@ -287,16 +291,23 @@ class CodecDescriptor(IndexBaseDescriptor):
|
||||
filename = os.path.basename(self.path)
|
||||
ext = filename.split(".")[-1]
|
||||
if filename.endswith(ext):
|
||||
name = filename[:-len(ext)]
|
||||
else: # should never hit this rather raise value error
|
||||
name = filename[: -len(ext)]
|
||||
else: # should never hit this rather raise value error
|
||||
name = filename
|
||||
return name
|
||||
|
||||
def alias(self, benchmark_io: BenchmarkIO):
|
||||
if hasattr(benchmark_io, "bucket"):
|
||||
return CodecDescriptor(desc_name=self.get_name(), bucket=benchmark_io.bucket, path=self.get_path(benchmark_io), d=self.d, metric=self.metric)
|
||||
return CodecDescriptor(desc_name=self.get_name(), d=self.d, metric=self.metric)
|
||||
|
||||
return CodecDescriptor(
|
||||
desc_name=self.get_name(),
|
||||
bucket=benchmark_io.bucket,
|
||||
path=self.get_path(benchmark_io),
|
||||
d=self.d,
|
||||
metric=self.metric,
|
||||
)
|
||||
return CodecDescriptor(
|
||||
desc_name=self.get_name(), d=self.d, metric=self.metric
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -316,21 +327,39 @@ class IndexDescriptor(IndexBaseDescriptor):
|
||||
|
||||
def get_name(self) -> str:
|
||||
if self.desc_name is None:
|
||||
self.desc_name = self.codec_desc.get_name() + self.database_desc.get_filename(prefix=IndexDescriptor.FILENAME_PREFIX)
|
||||
self.desc_name = (
|
||||
self.codec_desc.get_name()
|
||||
+ self.database_desc.get_filename(
|
||||
prefix=IndexDescriptor.FILENAME_PREFIX
|
||||
)
|
||||
)
|
||||
|
||||
return self.desc_name
|
||||
|
||||
def flat_name(self):
|
||||
if self.flat_desc_name is not None:
|
||||
return self.flat_desc_name
|
||||
self.flat_desc_name = self.codec_desc.flat_name() + self.database_desc.get_filename(prefix=IndexDescriptor.FILENAME_PREFIX)
|
||||
self.flat_desc_name = (
|
||||
self.codec_desc.flat_name()
|
||||
+ self.database_desc.get_filename(
|
||||
prefix=IndexDescriptor.FILENAME_PREFIX
|
||||
)
|
||||
)
|
||||
return self.flat_desc_name
|
||||
|
||||
# alias is used to refer when index is uploaded to blobstore and referred again
|
||||
def alias(self, benchmark_io: BenchmarkIO):
|
||||
if hasattr(benchmark_io, "bucket"):
|
||||
return IndexDescriptor(desc_name=self.get_name(), bucket=benchmark_io.bucket, path=self.get_path(benchmark_io), d=self.d, metric=self.metric)
|
||||
return IndexDescriptor(desc_name=self.get_name(), d=self.d, metric=self.metric)
|
||||
return IndexDescriptor(
|
||||
desc_name=self.get_name(),
|
||||
bucket=benchmark_io.bucket,
|
||||
path=self.get_path(benchmark_io),
|
||||
d=self.d,
|
||||
metric=self.metric,
|
||||
)
|
||||
return IndexDescriptor(
|
||||
desc_name=self.get_name(), d=self.d, metric=self.metric
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+23
-25
@@ -73,9 +73,8 @@ class IndexBase:
|
||||
def set_index_param(index, name, val, assert_same=False):
|
||||
index = faiss.downcast_index(index)
|
||||
val = int(val)
|
||||
if (
|
||||
isinstance(index, faiss.IndexPreTransform)
|
||||
or isinstance(index, faiss.IndexIDMap)
|
||||
if isinstance(index, faiss.IndexPreTransform) or isinstance(
|
||||
index, faiss.IndexIDMap
|
||||
):
|
||||
Index.set_index_param(index.index, name, val)
|
||||
return
|
||||
@@ -278,8 +277,8 @@ class IndexBase:
|
||||
):
|
||||
logger.info("knn_search: begin")
|
||||
if (
|
||||
search_parameters is not None and
|
||||
search_parameters.get("snap", 0) == 1
|
||||
search_parameters is not None
|
||||
and search_parameters.get("snap", 0) == 1
|
||||
):
|
||||
query_vectors = self.snap(query_vectors)
|
||||
filename = (
|
||||
@@ -329,9 +328,9 @@ class IndexBase:
|
||||
xq = self.io.get_dataset(query_vectors)
|
||||
(D, I), t, _ = timer("knn_search", lambda: index.search(xq, k))
|
||||
if (
|
||||
self.is_flat() or
|
||||
not hasattr(self, "database_vectors") or
|
||||
(self.database_vectors is None)
|
||||
self.is_flat()
|
||||
or not hasattr(self, "database_vectors")
|
||||
or (self.database_vectors is None)
|
||||
): # TODO
|
||||
R = D
|
||||
else:
|
||||
@@ -350,9 +349,7 @@ class IndexBase:
|
||||
"nlist": int(stats.nlist // repeat),
|
||||
"ndis": int(stats.ndis // repeat),
|
||||
"nheap_updates": int(stats.nheap_updates // repeat),
|
||||
"quantization_time": int(
|
||||
stats.quantization_time // repeat
|
||||
),
|
||||
"quantization_time": int(stats.quantization_time // repeat),
|
||||
"search_time": int(stats.search_time // repeat),
|
||||
}
|
||||
self.io.write_file(filename, ["D", "I", "R", "P"], [D, I, R, P])
|
||||
@@ -482,14 +479,12 @@ class IndexBase:
|
||||
):
|
||||
logger.info("range_search: begin")
|
||||
if (
|
||||
search_parameters is not None and
|
||||
search_parameters.get("snap", 0) == 1
|
||||
search_parameters is not None
|
||||
and search_parameters.get("snap", 0) == 1
|
||||
):
|
||||
query_vectors = self.snap(query_vectors)
|
||||
filename = (
|
||||
self.get_range_search_name(
|
||||
search_parameters, query_vectors, radius
|
||||
)
|
||||
self.get_range_search_name(search_parameters, query_vectors, radius)
|
||||
+ "zip"
|
||||
)
|
||||
if self.io.file_exist(filename):
|
||||
@@ -527,9 +522,7 @@ class IndexBase:
|
||||
R = D
|
||||
else:
|
||||
xb = self.io.get_dataset(self.database_vectors)
|
||||
R = refine_distances_range(
|
||||
lims, D, I, xq, xb, self.metric_type
|
||||
)
|
||||
R = refine_distances_range(lims, D, I, xq, xb, self.metric_type)
|
||||
P = {
|
||||
"time": t,
|
||||
"radius": radius,
|
||||
@@ -543,9 +536,7 @@ class IndexBase:
|
||||
"nlist": int(stats.nlist // repeat),
|
||||
"ndis": int(stats.ndis // repeat),
|
||||
"nheap_updates": int(stats.nheap_updates // repeat),
|
||||
"quantization_time": int(
|
||||
stats.quantization_time // repeat
|
||||
),
|
||||
"quantization_time": int(stats.quantization_time // repeat),
|
||||
"search_time": int(stats.search_time // repeat),
|
||||
}
|
||||
self.io.write_file(
|
||||
@@ -681,7 +672,8 @@ class Index(IndexBase):
|
||||
_, t, _ = timer(
|
||||
"add_with_ids",
|
||||
lambda: index.add_with_ids(
|
||||
xb, np.arange(len(xb), dtype='int32')),
|
||||
xb, np.arange(len(xb), dtype="int32")
|
||||
),
|
||||
once=True,
|
||||
)
|
||||
else:
|
||||
@@ -934,7 +926,9 @@ class IndexFromFactory(Index):
|
||||
assert self.training_vectors is not None
|
||||
codec_name += self.training_vectors.get_filename("xt")
|
||||
if self.construction_params is not None:
|
||||
codec_name += IndexBaseDescriptor.param_dict_list_to_name(self.construction_params)
|
||||
codec_name += IndexBaseDescriptor.param_dict_list_to_name(
|
||||
self.construction_params
|
||||
)
|
||||
self.codec_name = codec_name
|
||||
return self.codec_name
|
||||
|
||||
@@ -969,7 +963,11 @@ class IndexFromFactory(Index):
|
||||
assert codec_size is not None
|
||||
meta = {
|
||||
"training_time": training_time,
|
||||
"training_size": self.training_vectors.num_vectors if self.training_vectors else 0,
|
||||
"training_size": (
|
||||
self.training_vectors.num_vectors
|
||||
if self.training_vectors
|
||||
else 0
|
||||
),
|
||||
"codec_size": codec_size,
|
||||
"sa_code_size": self.get_sa_code_size(codec),
|
||||
"code_size": self.get_code_size(codec),
|
||||
|
||||
@@ -70,9 +70,9 @@ class Optimizer:
|
||||
evaluation="knn",
|
||||
accuracy_metric="knn_intersection",
|
||||
min_accuracy=min_accuracy,
|
||||
name_filter=None
|
||||
if include_flat
|
||||
else (lambda n: not n.startswith("Flat")),
|
||||
name_filter=(
|
||||
None if include_flat else (lambda n: not n.startswith("Flat"))
|
||||
),
|
||||
pareto_mode=ParetoMode.GLOBAL,
|
||||
pareto_metric=pareto_metric,
|
||||
)
|
||||
@@ -103,7 +103,9 @@ class Optimizer:
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
descs = [IndexDescriptorClassic(factory="Flat"),] + [
|
||||
descs = [
|
||||
IndexDescriptorClassic(factory="Flat"),
|
||||
] + [
|
||||
IndexDescriptorClassic(
|
||||
factory="HNSW32",
|
||||
construction_params=[{"efConstruction": 2**i}],
|
||||
@@ -229,7 +231,8 @@ class Optimizer:
|
||||
(None, "SQbf16"),
|
||||
(None, "SQ8"),
|
||||
(None, "SQ8_direct_signed"),
|
||||
] + [
|
||||
]
|
||||
+ [
|
||||
(f"OPQ{M}_{M * dim}", f"PQ{M}x{b}")
|
||||
for M in [8, 12, 16, 32, 48, 64, 96, 128, 192, 256]
|
||||
if d % M == 0
|
||||
@@ -237,7 +240,8 @@ class Optimizer:
|
||||
if M * dim <= d
|
||||
for b in range(4, 14, 2)
|
||||
if M * b < d * 8 # smaller than SQ8
|
||||
] + [
|
||||
]
|
||||
+ [
|
||||
(None, f"PQ{M}x{b}")
|
||||
for M in [8, 12, 16, 32, 48, 64, 96, 128, 192, 256]
|
||||
if d % M == 0
|
||||
@@ -257,9 +261,11 @@ class Optimizer:
|
||||
_, filtered = self.benchmark_and_filter_candidates(
|
||||
index_descs=[
|
||||
IndexDescriptorClassic(
|
||||
factory=f"IVF{nlist},{pq}"
|
||||
if opq is None
|
||||
else f"{opq},IVF{nlist},{pq}",
|
||||
factory=(
|
||||
f"IVF{nlist},{pq}"
|
||||
if opq is None
|
||||
else f"{opq},IVF{nlist},{pq}"
|
||||
),
|
||||
search_params={
|
||||
"nprobe": nprobe,
|
||||
},
|
||||
|
||||
@@ -91,14 +91,18 @@ def refine_distances_range(
|
||||
with ThreadPool(32) as pool:
|
||||
R = pool.map(
|
||||
lambda i: (
|
||||
np.sum(np.square(xq[i] - xb[I[lims[i] : lims[i + 1]]]), axis=1)
|
||||
if metric == faiss.METRIC_L2
|
||||
else np.tensordot(
|
||||
xq[i], xb[I[lims[i] : lims[i + 1]]], axes=(0, 1)
|
||||
(
|
||||
np.sum(
|
||||
np.square(xq[i] - xb[I[lims[i] : lims[i + 1]]]), axis=1
|
||||
)
|
||||
if metric == faiss.METRIC_L2
|
||||
else np.tensordot(
|
||||
xq[i], xb[I[lims[i] : lims[i + 1]]], axes=(0, 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
if lims[i + 1] > lims[i]
|
||||
else [],
|
||||
if lims[i + 1] > lims[i]
|
||||
else []
|
||||
),
|
||||
range(len(lims) - 1),
|
||||
)
|
||||
return np.hstack(R)
|
||||
|
||||
+123
-79
@@ -9,97 +9,130 @@ import os
|
||||
|
||||
from faiss.benchs.bench_fw.benchmark import Benchmark
|
||||
from faiss.benchs.bench_fw.benchmark_io import BenchmarkIO
|
||||
from faiss.benchs.bench_fw.descriptors import DatasetDescriptor, IndexDescriptorClassic
|
||||
from faiss.benchs.bench_fw.descriptors import (
|
||||
DatasetDescriptor,
|
||||
IndexDescriptorClassic,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def factory_factory(d):
|
||||
return [
|
||||
("SQ4", None, 256 * (2 ** 10), None),
|
||||
("SQ8", None, 256 * (2 ** 10), None),
|
||||
("SQfp16", None, 256 * (2 ** 10), None),
|
||||
("ITQ64,LSH", None, 256 * (2 ** 10), None),
|
||||
("Pad128,ITQ128,LSH", None, 256 * (2 ** 10), None),
|
||||
("Pad256,ITQ256,LSH", None, 256 * (2 ** 10), None),
|
||||
] + [
|
||||
(f"OPQ32_128,Residual2x14,PQ32x{b}", None, 256 * (2 ** 14), None)
|
||||
for b in range(8, 16, 2)
|
||||
] + [
|
||||
(f"PCAR{2 ** d_out},SQ{b}", None, 256 * (2 ** 10), None)
|
||||
for d_out in range(6, 11)
|
||||
if 2 ** d_out <= d
|
||||
for b in [4, 8]
|
||||
] + [
|
||||
(f"OPQ{M}_{M * dim},PQ{M}x{b}", None, 256 * (2 ** b), None)
|
||||
for M in [8, 12, 16, 32, 64, 128]
|
||||
for dim in [2, 4, 6, 8, 12, 16]
|
||||
if M * dim <= d
|
||||
for b in range(8, 16, 2)
|
||||
] + [
|
||||
(f"RQ{cs // b}x{b}", [{"max_beam_size": 32}], 256 * (2 ** b), {"max_beam_size": bs, "use_beam_LUT": bl})
|
||||
for cs in [64, 128, 256, 512]
|
||||
for b in [6, 8, 10, 12]
|
||||
for bs in [1, 2, 4, 8, 16, 32]
|
||||
for bl in [0, 1]
|
||||
if cs // b > 1
|
||||
if cs // b < 65
|
||||
if cs < d * 8 * 2
|
||||
] + [
|
||||
(f"LSQ{cs // b}x{b}", [{"encode_ils_iters": 16}], 256 * (2 ** b), {"encode_ils_iters": eii, "lsq_gpu": lg})
|
||||
for cs in [64, 128, 256, 512]
|
||||
for b in [6, 8, 10, 12]
|
||||
for eii in [2, 4, 8, 16]
|
||||
for lg in [0, 1]
|
||||
if cs // b > 1
|
||||
if cs // b < 65
|
||||
if cs < d * 8 * 2
|
||||
] + [
|
||||
(f"PRQ{sub}x{cs // sub // b}x{b}", [{"max_beam_size": 32}], 256 * (2 ** b), {"max_beam_size": bs, "use_beam_LUT": bl})
|
||||
for sub in [2, 3, 4, 8, 16, 32]
|
||||
for cs in [64, 96, 128, 192, 256, 384, 512, 768, 1024, 2048]
|
||||
for b in [6, 8, 10, 12]
|
||||
for bs in [1, 2, 4, 8, 16, 32]
|
||||
for bl in [0, 1]
|
||||
if cs // sub // b > 1
|
||||
if cs // sub // b < 65
|
||||
if cs < d * 8 * 2
|
||||
if d % sub == 0
|
||||
] + [
|
||||
(f"PLSQ{sub}x{cs // sub // b}x{b}", [{"encode_ils_iters": 16}], 256 * (2 ** b), {"encode_ils_iters": eii, "lsq_gpu": lg})
|
||||
for sub in [2, 3, 4, 8, 16, 32]
|
||||
for cs in [64, 128, 256, 512, 1024, 2048]
|
||||
for b in [6, 8, 10, 12]
|
||||
for eii in [2, 4, 8, 16]
|
||||
for lg in [0, 1]
|
||||
if cs // sub // b > 1
|
||||
if cs // sub // b < 65
|
||||
if cs < d * 8 * 2
|
||||
if d % sub == 0
|
||||
]
|
||||
return (
|
||||
[
|
||||
("SQ4", None, 256 * (2**10), None),
|
||||
("SQ8", None, 256 * (2**10), None),
|
||||
("SQfp16", None, 256 * (2**10), None),
|
||||
("ITQ64,LSH", None, 256 * (2**10), None),
|
||||
("Pad128,ITQ128,LSH", None, 256 * (2**10), None),
|
||||
("Pad256,ITQ256,LSH", None, 256 * (2**10), None),
|
||||
]
|
||||
+ [
|
||||
(f"OPQ32_128,Residual2x14,PQ32x{b}", None, 256 * (2**14), None)
|
||||
for b in range(8, 16, 2)
|
||||
]
|
||||
+ [
|
||||
(f"PCAR{2 ** d_out},SQ{b}", None, 256 * (2**10), None)
|
||||
for d_out in range(6, 11)
|
||||
if 2**d_out <= d
|
||||
for b in [4, 8]
|
||||
]
|
||||
+ [
|
||||
(f"OPQ{M}_{M * dim},PQ{M}x{b}", None, 256 * (2**b), None)
|
||||
for M in [8, 12, 16, 32, 64, 128]
|
||||
for dim in [2, 4, 6, 8, 12, 16]
|
||||
if M * dim <= d
|
||||
for b in range(8, 16, 2)
|
||||
]
|
||||
+ [
|
||||
(
|
||||
f"RQ{cs // b}x{b}",
|
||||
[{"max_beam_size": 32}],
|
||||
256 * (2**b),
|
||||
{"max_beam_size": bs, "use_beam_LUT": bl},
|
||||
)
|
||||
for cs in [64, 128, 256, 512]
|
||||
for b in [6, 8, 10, 12]
|
||||
for bs in [1, 2, 4, 8, 16, 32]
|
||||
for bl in [0, 1]
|
||||
if cs // b > 1
|
||||
if cs // b < 65
|
||||
if cs < d * 8 * 2
|
||||
]
|
||||
+ [
|
||||
(
|
||||
f"LSQ{cs // b}x{b}",
|
||||
[{"encode_ils_iters": 16}],
|
||||
256 * (2**b),
|
||||
{"encode_ils_iters": eii, "lsq_gpu": lg},
|
||||
)
|
||||
for cs in [64, 128, 256, 512]
|
||||
for b in [6, 8, 10, 12]
|
||||
for eii in [2, 4, 8, 16]
|
||||
for lg in [0, 1]
|
||||
if cs // b > 1
|
||||
if cs // b < 65
|
||||
if cs < d * 8 * 2
|
||||
]
|
||||
+ [
|
||||
(
|
||||
f"PRQ{sub}x{cs // sub // b}x{b}",
|
||||
[{"max_beam_size": 32}],
|
||||
256 * (2**b),
|
||||
{"max_beam_size": bs, "use_beam_LUT": bl},
|
||||
)
|
||||
for sub in [2, 3, 4, 8, 16, 32]
|
||||
for cs in [64, 96, 128, 192, 256, 384, 512, 768, 1024, 2048]
|
||||
for b in [6, 8, 10, 12]
|
||||
for bs in [1, 2, 4, 8, 16, 32]
|
||||
for bl in [0, 1]
|
||||
if cs // sub // b > 1
|
||||
if cs // sub // b < 65
|
||||
if cs < d * 8 * 2
|
||||
if d % sub == 0
|
||||
]
|
||||
+ [
|
||||
(
|
||||
f"PLSQ{sub}x{cs // sub // b}x{b}",
|
||||
[{"encode_ils_iters": 16}],
|
||||
256 * (2**b),
|
||||
{"encode_ils_iters": eii, "lsq_gpu": lg},
|
||||
)
|
||||
for sub in [2, 3, 4, 8, 16, 32]
|
||||
for cs in [64, 128, 256, 512, 1024, 2048]
|
||||
for b in [6, 8, 10, 12]
|
||||
for eii in [2, 4, 8, 16]
|
||||
for lg in [0, 1]
|
||||
if cs // sub // b > 1
|
||||
if cs // sub // b < 65
|
||||
if cs < d * 8 * 2
|
||||
if d % sub == 0
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def run_local(rp):
|
||||
bio, d, tablename, distance_metric = rp
|
||||
if tablename == "contriever":
|
||||
training_vectors=DatasetDescriptor(
|
||||
tablename="training_set.npy"
|
||||
)
|
||||
database_vectors=DatasetDescriptor(
|
||||
training_vectors = DatasetDescriptor(tablename="training_set.npy")
|
||||
database_vectors = DatasetDescriptor(
|
||||
tablename="database1M.npy",
|
||||
)
|
||||
query_vectors=DatasetDescriptor(
|
||||
query_vectors = DatasetDescriptor(
|
||||
tablename="queries.npy",
|
||||
)
|
||||
else:
|
||||
training_vectors=DatasetDescriptor(
|
||||
namespace="std_t", tablename=tablename,
|
||||
training_vectors = DatasetDescriptor(
|
||||
namespace="std_t",
|
||||
tablename=tablename,
|
||||
)
|
||||
database_vectors=DatasetDescriptor(
|
||||
namespace="std_d", tablename=tablename,
|
||||
database_vectors = DatasetDescriptor(
|
||||
namespace="std_d",
|
||||
tablename=tablename,
|
||||
)
|
||||
query_vectors=DatasetDescriptor(
|
||||
namespace="std_q", tablename=tablename,
|
||||
query_vectors = DatasetDescriptor(
|
||||
namespace="std_q",
|
||||
tablename=tablename,
|
||||
)
|
||||
|
||||
benchmark = Benchmark(
|
||||
@@ -114,22 +147,33 @@ def run_local(rp):
|
||||
training_size=training_size,
|
||||
search_params=search_params,
|
||||
)
|
||||
for factory, construction_params, training_size, search_params in factory_factory(d)
|
||||
for factory, construction_params, training_size, search_params in factory_factory(
|
||||
d
|
||||
)
|
||||
],
|
||||
k=1,
|
||||
distance_metric=distance_metric,
|
||||
)
|
||||
benchmark.set_io(bio)
|
||||
benchmark.benchmark(result_file="result.json", train=True, reconstruct=False, knn=False, range=False)
|
||||
benchmark.benchmark(
|
||||
result_file="result.json",
|
||||
train=True,
|
||||
reconstruct=False,
|
||||
knn=False,
|
||||
range=False,
|
||||
)
|
||||
|
||||
|
||||
def run(bio, d, tablename, distance_metric):
|
||||
bio.launch_jobs(run_local, [(bio, d, tablename, distance_metric)], local=True)
|
||||
bio.launch_jobs(
|
||||
run_local, [(bio, d, tablename, distance_metric)], local=True
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('experiment')
|
||||
parser.add_argument('path')
|
||||
parser.add_argument("experiment")
|
||||
parser.add_argument("path")
|
||||
args = parser.parse_args()
|
||||
assert os.path.exists(args.path)
|
||||
path = os.path.join(args.path, args.experiment)
|
||||
|
||||
+49
-25
@@ -26,9 +26,7 @@ def sift1M(bio):
|
||||
database_vectors=DatasetDescriptor(
|
||||
namespace="std_d", tablename="sift1M"
|
||||
),
|
||||
query_vectors=DatasetDescriptor(
|
||||
namespace="std_q", tablename="sift1M"
|
||||
),
|
||||
query_vectors=DatasetDescriptor(namespace="std_q", tablename="sift1M"),
|
||||
index_descs=[
|
||||
IndexDescriptorClassic(
|
||||
factory=f"IVF{2 ** nlist},Flat",
|
||||
@@ -39,7 +37,14 @@ def sift1M(bio):
|
||||
distance_metric="L2",
|
||||
)
|
||||
benchmark.io = bio
|
||||
benchmark.benchmark(result_file="result.json", local=True, train=True, reconstruct=False, knn=True, range=False)
|
||||
benchmark.benchmark(
|
||||
result_file="result.json",
|
||||
local=True,
|
||||
train=True,
|
||||
reconstruct=False,
|
||||
knn=True,
|
||||
range=False,
|
||||
)
|
||||
|
||||
|
||||
def bigann(bio):
|
||||
@@ -58,41 +63,52 @@ def bigann(bio):
|
||||
index_descs=[
|
||||
IndexDescriptorClassic(
|
||||
factory=f"IVF{2 ** nlist},Flat",
|
||||
) for nlist in range(11, 19)
|
||||
] + [
|
||||
)
|
||||
for nlist in range(11, 19)
|
||||
]
|
||||
+ [
|
||||
IndexDescriptorClassic(
|
||||
factory=f"IVF{2 ** nlist}_HNSW32,Flat",
|
||||
construction_params=[None, {"efConstruction": 200, "efSearch": 40}],
|
||||
) for nlist in range(11, 19)
|
||||
construction_params=[
|
||||
None,
|
||||
{"efConstruction": 200, "efSearch": 40},
|
||||
],
|
||||
)
|
||||
for nlist in range(11, 19)
|
||||
],
|
||||
k=1,
|
||||
distance_metric="L2",
|
||||
)
|
||||
benchmark.set_io(bio)
|
||||
benchmark.benchmark(f"result{scale}.json", local=False, train=True, reconstruct=False, knn=True, range=False)
|
||||
benchmark.benchmark(
|
||||
f"result{scale}.json",
|
||||
local=False,
|
||||
train=True,
|
||||
reconstruct=False,
|
||||
knn=True,
|
||||
range=False,
|
||||
)
|
||||
|
||||
|
||||
def ssnpp(bio):
|
||||
benchmark = Benchmark(
|
||||
num_threads=32,
|
||||
training_vectors=DatasetDescriptor(
|
||||
tablename="ssnpp_training_5M.npy"
|
||||
),
|
||||
database_vectors=DatasetDescriptor(
|
||||
tablename="ssnpp_database_5M.npy"
|
||||
),
|
||||
query_vectors=DatasetDescriptor(
|
||||
tablename="ssnpp_queries_10K.npy"
|
||||
),
|
||||
training_vectors=DatasetDescriptor(tablename="ssnpp_training_5M.npy"),
|
||||
database_vectors=DatasetDescriptor(tablename="ssnpp_database_5M.npy"),
|
||||
query_vectors=DatasetDescriptor(tablename="ssnpp_queries_10K.npy"),
|
||||
index_descs=[
|
||||
IndexDescriptorClassic(
|
||||
factory=f"IVF{2 ** nlist},PQ256x4fs,Refine(SQfp16)",
|
||||
) for nlist in range(9, 16)
|
||||
] + [
|
||||
)
|
||||
for nlist in range(9, 16)
|
||||
]
|
||||
+ [
|
||||
IndexDescriptorClassic(
|
||||
factory=f"IVF{2 ** nlist},Flat",
|
||||
) for nlist in range(9, 16)
|
||||
] + [
|
||||
)
|
||||
for nlist in range(9, 16)
|
||||
]
|
||||
+ [
|
||||
IndexDescriptorClassic(
|
||||
factory=f"PQ256x4fs,Refine(SQfp16)",
|
||||
),
|
||||
@@ -104,12 +120,20 @@ def ssnpp(bio):
|
||||
distance_metric="L2",
|
||||
)
|
||||
benchmark.set_io(bio)
|
||||
benchmark.benchmark("result.json", local=False, train=True, reconstruct=False, knn=True, range=False)
|
||||
benchmark.benchmark(
|
||||
"result.json",
|
||||
local=False,
|
||||
train=True,
|
||||
reconstruct=False,
|
||||
knn=True,
|
||||
range=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('experiment')
|
||||
parser.add_argument('path')
|
||||
parser.add_argument("experiment")
|
||||
parser.add_argument("path")
|
||||
args = parser.parse_args()
|
||||
assert os.path.exists(args.path)
|
||||
path = os.path.join(args.path, args.experiment)
|
||||
|
||||
+31
-21
@@ -9,7 +9,10 @@ import os
|
||||
|
||||
from faiss.benchs.bench_fw.benchmark import Benchmark
|
||||
from faiss.benchs.bench_fw.benchmark_io import BenchmarkIO
|
||||
from faiss.benchs.bench_fw.descriptors import DatasetDescriptor, IndexDescriptorClassic
|
||||
from faiss.benchs.bench_fw.descriptors import (
|
||||
DatasetDescriptor,
|
||||
IndexDescriptorClassic,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@@ -34,25 +37,25 @@ def ssnpp(bio):
|
||||
[0.15, 0.923],
|
||||
[0.2, 0.887],
|
||||
[0.25, 0.801],
|
||||
[0.3, 0.729],
|
||||
[0.35, 0.651],
|
||||
[0.4, 0.55],
|
||||
[0.45, 0.459],
|
||||
[0.5, 0.372],
|
||||
[0.55, 0.283],
|
||||
[0.6, 0.189],
|
||||
[0.65, 0.143],
|
||||
[0.7, 0.106],
|
||||
[0.75, 0.116],
|
||||
[0.8, 0.088],
|
||||
[0.3, 0.729],
|
||||
[0.35, 0.651],
|
||||
[0.4, 0.55],
|
||||
[0.45, 0.459],
|
||||
[0.5, 0.372],
|
||||
[0.55, 0.283],
|
||||
[0.6, 0.189],
|
||||
[0.65, 0.143],
|
||||
[0.7, 0.106],
|
||||
[0.75, 0.116],
|
||||
[0.8, 0.088],
|
||||
[0.85, 0.064],
|
||||
[0.9, 0.05],
|
||||
[0.95, 0.04],
|
||||
[1.0, 0.028],
|
||||
[1.05, 0.02],
|
||||
[0.9, 0.05],
|
||||
[0.95, 0.04],
|
||||
[1.0, 0.028],
|
||||
[1.05, 0.02],
|
||||
[1.1, 0.013],
|
||||
[1.15, 0.007],
|
||||
[1.2, 0.004],
|
||||
[1.15, 0.007],
|
||||
[1.2, 0.004],
|
||||
[1.3, 0],
|
||||
]
|
||||
},
|
||||
@@ -66,13 +69,20 @@ def ssnpp(bio):
|
||||
range_ref_index_desc="Flat",
|
||||
)
|
||||
benchmark.set_io(bio)
|
||||
benchmark.benchmark("result.json", local=False, train=True, reconstruct=False, knn=False, range=True)
|
||||
benchmark.benchmark(
|
||||
"result.json",
|
||||
local=False,
|
||||
train=True,
|
||||
reconstruct=False,
|
||||
knn=False,
|
||||
range=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('experiment')
|
||||
parser.add_argument('path')
|
||||
parser.add_argument("experiment")
|
||||
parser.add_argument("path")
|
||||
args = parser.parse_args()
|
||||
assert os.path.exists(args.path)
|
||||
path = os.path.join(args.path, args.experiment)
|
||||
|
||||
+12
-12
@@ -18,17 +18,17 @@ n = 10000
|
||||
nq = 200
|
||||
n_trials = 15
|
||||
|
||||
print(f'n={n} vectors, nq={nq} queries, {n_trials} speed trials')
|
||||
print(f"n={n} vectors, nq={nq} queries, {n_trials} speed trials")
|
||||
|
||||
# --- Speed benchmark ---
|
||||
print('\n=== Speed ===')
|
||||
print("\n=== Speed ===")
|
||||
print(f'{"dim":>6s} {"HR (ms)":>10s} {"RRM (ms)":>10s} {"speedup":>8s}')
|
||||
print('-' * 42)
|
||||
print("-" * 42)
|
||||
|
||||
dims = [64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192]
|
||||
for d in dims:
|
||||
np.random.seed(42)
|
||||
x = np.random.randn(n, d).astype('float32')
|
||||
x = np.random.randn(n, d).astype("float32")
|
||||
|
||||
hr = faiss.HadamardRotation(d, 42)
|
||||
hr.apply(x[:100]) # warmup
|
||||
@@ -52,17 +52,17 @@ for d in dims:
|
||||
rr_ms = sorted(times_rr)[1]
|
||||
|
||||
speedup = rr_ms / hr_ms
|
||||
print(f'{d:6d} {hr_ms:10.2f} {rr_ms:10.2f} {speedup:7.1f}x')
|
||||
print(f"{d:6d} {hr_ms:10.2f} {rr_ms:10.2f} {speedup:7.1f}x")
|
||||
|
||||
# --- Recall benchmark with IVF (where rotation actually matters) ---
|
||||
print(f'\n=== Recall@1 with IVF (L2, n={n}, nq={nq}, k=1) ===')
|
||||
print(f"\n=== Recall@1 with IVF (L2, n={n}, nq={nq}, k=1) ===")
|
||||
print(f'{"dim":>6s} {"HR R@1":>8s} {"RRM R@1":>8s} {"none R@1":>8s}')
|
||||
print('-' * 40)
|
||||
print("-" * 40)
|
||||
|
||||
for d in [64, 128, 256, 768, 1024, 2048, 4096]:
|
||||
np.random.seed(42)
|
||||
xb = np.random.randn(n, d).astype('float32')
|
||||
xq = np.random.randn(nq, d).astype('float32')
|
||||
xb = np.random.randn(n, d).astype("float32")
|
||||
xq = np.random.randn(nq, d).astype("float32")
|
||||
|
||||
# Ground truth (brute force, no transform)
|
||||
gt_index = faiss.IndexFlatL2(d)
|
||||
@@ -72,9 +72,9 @@ for d in [64, 128, 256, 768, 1024, 2048, 4096]:
|
||||
nlist = 64
|
||||
recalls = {}
|
||||
for label, factory_str in [
|
||||
('HR', f'HR,IVF{nlist},Flat'),
|
||||
('RRM', f'RR,IVF{nlist},Flat'),
|
||||
('none', f'IVF{nlist},Flat'),
|
||||
("HR", f"HR,IVF{nlist},Flat"),
|
||||
("RRM", f"RR,IVF{nlist},Flat"),
|
||||
("none", f"IVF{nlist},Flat"),
|
||||
]:
|
||||
index = faiss.index_factory(d, factory_str)
|
||||
index.train(xb)
|
||||
|
||||
+132
-113
@@ -21,7 +21,8 @@ from datasets import ivecs_read
|
||||
|
||||
|
||||
def usage():
|
||||
print("""
|
||||
print(
|
||||
"""
|
||||
|
||||
Usage: bench_gpu_1bn.py dataset indextype [options]
|
||||
|
||||
@@ -60,7 +61,9 @@ indextype: any index type supported by index_factory that runs on GPU.
|
||||
%d will be replaced with the nprobe
|
||||
-oD xx%d.npy output the search result distances to this file
|
||||
|
||||
""", file=sys.stderr)
|
||||
""",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -90,33 +93,33 @@ args = sys.argv[1:]
|
||||
|
||||
while args:
|
||||
a = args.pop(0)
|
||||
if a == '-h':
|
||||
if a == "-h":
|
||||
usage()
|
||||
elif a == '-ngpu':
|
||||
elif a == "-ngpu":
|
||||
ngpu = int(args.pop(0))
|
||||
elif a == '-R':
|
||||
elif a == "-R":
|
||||
replicas = int(args.pop(0))
|
||||
elif a == '-noptables':
|
||||
elif a == "-noptables":
|
||||
use_precomputed_tables = False
|
||||
elif a == '-abs':
|
||||
elif a == "-abs":
|
||||
add_batch_size = int(args.pop(0))
|
||||
elif a == '-qbs':
|
||||
elif a == "-qbs":
|
||||
query_batch_size = int(args.pop(0))
|
||||
elif a == '-nnn':
|
||||
elif a == "-nnn":
|
||||
nnn = int(args.pop(0))
|
||||
elif a == '-tempmem':
|
||||
elif a == "-tempmem":
|
||||
tempmem = int(args.pop(0))
|
||||
elif a == '-nocache':
|
||||
elif a == "-nocache":
|
||||
use_cache = False
|
||||
elif a == '-knngraph':
|
||||
elif a == "-knngraph":
|
||||
knngraph = True
|
||||
elif a == '-altadd':
|
||||
elif a == "-altadd":
|
||||
altadd = True
|
||||
elif a == '-float16':
|
||||
elif a == "-float16":
|
||||
use_float16 = True
|
||||
elif a == '-nprobe':
|
||||
nprobes = [int(x) for x in args.pop(0).split(',')]
|
||||
elif a == '-max_add':
|
||||
elif a == "-nprobe":
|
||||
nprobes = [int(x) for x in args.pop(0).split(",")]
|
||||
elif a == "-max_add":
|
||||
max_add = int(args.pop(0))
|
||||
elif not dbname:
|
||||
dbname = a
|
||||
@@ -126,7 +129,7 @@ while args:
|
||||
print("argument %s unknown" % a, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cacheroot = '/tmp/bench_gpu_1bn'
|
||||
cacheroot = "/tmp/bench_gpu_1bn"
|
||||
|
||||
if not os.path.isdir(cacheroot):
|
||||
print("%s does not exist, creating it" % cacheroot)
|
||||
@@ -141,14 +144,14 @@ if not os.path.isdir(cacheroot):
|
||||
|
||||
|
||||
def mmap_fvecs(fname):
|
||||
x = np.memmap(fname, dtype='int32', mode='r')
|
||||
x = np.memmap(fname, dtype="int32", mode="r")
|
||||
d = x[0]
|
||||
return x.view('float32').reshape(-1, d + 1)[:, 1:]
|
||||
return x.view("float32").reshape(-1, d + 1)[:, 1:]
|
||||
|
||||
|
||||
def mmap_bvecs(fname):
|
||||
x = np.memmap(fname, dtype='uint8', mode='r')
|
||||
d = x[:4].view('int32')[0]
|
||||
x = np.memmap(fname, dtype="uint8", mode="r")
|
||||
d = x[:4].view("int32")[0]
|
||||
return x.reshape(-1, d + 4)[:, 4:]
|
||||
|
||||
|
||||
@@ -158,7 +161,7 @@ def rate_limited_imap(f, l):
|
||||
pool = ThreadPool(1)
|
||||
res = None
|
||||
for i in l:
|
||||
res_next = pool.apply_async(f, (i, ))
|
||||
res_next = pool.apply_async(f, (i,))
|
||||
if res:
|
||||
yield res.get()
|
||||
res = res_next
|
||||
@@ -176,16 +179,15 @@ class IdentPreproc:
|
||||
|
||||
|
||||
def sanitize(x):
|
||||
""" convert array to a c-contiguous float array """
|
||||
return np.ascontiguousarray(x.astype('float32'))
|
||||
"""convert array to a c-contiguous float array"""
|
||||
return np.ascontiguousarray(x.astype("float32"))
|
||||
|
||||
|
||||
def dataset_iterator(x, preproc, bs):
|
||||
""" iterate over the lines of x in blocks of size bs"""
|
||||
"""iterate over the lines of x in blocks of size bs"""
|
||||
|
||||
nb = x.shape[0]
|
||||
block_ranges = [(i0, min(nb, i0 + bs))
|
||||
for i0 in range(0, nb, bs)]
|
||||
block_ranges = [(i0, min(nb, i0 + bs)) for i0 in range(0, nb, bs)]
|
||||
|
||||
def prepare_block(i01):
|
||||
i0, i1 = i01
|
||||
@@ -196,14 +198,17 @@ def dataset_iterator(x, preproc, bs):
|
||||
|
||||
|
||||
def eval_intersection_measure(gt_I, I):
|
||||
""" measure intersection measure (used for knngraph)"""
|
||||
"""measure intersection measure (used for knngraph)"""
|
||||
inter = 0
|
||||
rank = I.shape[1]
|
||||
assert gt_I.shape[1] >= rank
|
||||
for q in range(nq_gt):
|
||||
inter += faiss.ranklist_intersection_size(
|
||||
rank, faiss.swig_ptr(gt_I[q, :]),
|
||||
rank, faiss.swig_ptr(I[q, :].astype('int64')))
|
||||
rank,
|
||||
faiss.swig_ptr(gt_I[q, :]),
|
||||
rank,
|
||||
faiss.swig_ptr(I[q, :].astype("int64")),
|
||||
)
|
||||
return inter / float(rank * nq_gt)
|
||||
|
||||
|
||||
@@ -213,28 +218,28 @@ def eval_intersection_measure(gt_I, I):
|
||||
|
||||
print("Preparing dataset", dbname)
|
||||
|
||||
if dbname.startswith('SIFT'):
|
||||
if dbname.startswith("SIFT"):
|
||||
# SIFT1M to SIFT1000M
|
||||
dbsize = int(dbname[4:-1])
|
||||
xb = mmap_bvecs('bigann/bigann_base.bvecs')
|
||||
xq = mmap_bvecs('bigann/bigann_query.bvecs')
|
||||
xt = mmap_bvecs('bigann/bigann_learn.bvecs')
|
||||
xb = mmap_bvecs("bigann/bigann_base.bvecs")
|
||||
xq = mmap_bvecs("bigann/bigann_query.bvecs")
|
||||
xt = mmap_bvecs("bigann/bigann_learn.bvecs")
|
||||
|
||||
# trim xb to correct size
|
||||
xb = xb[:dbsize * 1000 * 1000]
|
||||
xb = xb[: dbsize * 1000 * 1000]
|
||||
|
||||
gt_I = ivecs_read('bigann/gnd/idx_%dM.ivecs' % dbsize)
|
||||
gt_I = ivecs_read("bigann/gnd/idx_%dM.ivecs" % dbsize)
|
||||
|
||||
elif dbname == 'Deep1B':
|
||||
xb = mmap_fvecs('deep1b/base.fvecs')
|
||||
xq = mmap_fvecs('deep1b/deep1B_queries.fvecs')
|
||||
xt = mmap_fvecs('deep1b/learn.fvecs')
|
||||
elif dbname == "Deep1B":
|
||||
xb = mmap_fvecs("deep1b/base.fvecs")
|
||||
xq = mmap_fvecs("deep1b/deep1B_queries.fvecs")
|
||||
xt = mmap_fvecs("deep1b/learn.fvecs")
|
||||
# deep1B's train is outrageously big
|
||||
xt = xt[:10 * 1000 * 1000]
|
||||
gt_I = ivecs_read('deep1b/deep1B_groundtruth.ivecs')
|
||||
xt = xt[: 10 * 1000 * 1000]
|
||||
gt_I = ivecs_read("deep1b/deep1B_groundtruth.ivecs")
|
||||
|
||||
else:
|
||||
print('unknown dataset', dbname, file=sys.stderr)
|
||||
print("unknown dataset", dbname, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -250,9 +255,10 @@ if knngraph:
|
||||
gt_I = None
|
||||
|
||||
|
||||
print("sizes: B %s Q %s T %s gt %s" % (
|
||||
xb.shape, xq.shape, xt.shape,
|
||||
gt_I.shape if gt_I is not None else None))
|
||||
print(
|
||||
"sizes: B %s Q %s T %s gt %s"
|
||||
% (xb.shape, xq.shape, xt.shape, gt_I.shape if gt_I is not None else None)
|
||||
)
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -263,13 +269,13 @@ print("sizes: B %s Q %s T %s gt %s" % (
|
||||
#################################################################
|
||||
|
||||
|
||||
pat = re.compile('(OPQ[0-9]+(_[0-9]+)?,|PCAR[0-9]+,)?' +
|
||||
'(IVF[0-9]+),' +
|
||||
'(PQ[0-9]+|Flat)')
|
||||
pat = re.compile(
|
||||
"(OPQ[0-9]+(_[0-9]+)?,|PCAR[0-9]+,)?" + "(IVF[0-9]+)," + "(PQ[0-9]+|Flat)"
|
||||
)
|
||||
|
||||
matchobject = pat.match(index_key)
|
||||
|
||||
assert matchobject, 'could not parse ' + index_key
|
||||
assert matchobject, "could not parse " + index_key
|
||||
|
||||
mog = matchobject.groups()
|
||||
|
||||
@@ -279,26 +285,41 @@ pqflat_str = mog[3]
|
||||
|
||||
ncent = int(ivf_str[3:])
|
||||
|
||||
prefix = ''
|
||||
prefix = ""
|
||||
|
||||
if knngraph:
|
||||
gt_cachefile = '%s/BK_gt_%s.npy' % (cacheroot, dbname)
|
||||
prefix = 'BK_'
|
||||
gt_cachefile = "%s/BK_gt_%s.npy" % (cacheroot, dbname)
|
||||
prefix = "BK_"
|
||||
# files must be kept distinct because the training set is not the
|
||||
# same for the knngraph
|
||||
|
||||
if preproc_str:
|
||||
preproc_cachefile = '%s/%spreproc_%s_%s.vectrans' % (
|
||||
cacheroot, prefix, dbname, preproc_str[:-1])
|
||||
preproc_cachefile = "%s/%spreproc_%s_%s.vectrans" % (
|
||||
cacheroot,
|
||||
prefix,
|
||||
dbname,
|
||||
preproc_str[:-1],
|
||||
)
|
||||
else:
|
||||
preproc_cachefile = None
|
||||
preproc_str = ''
|
||||
preproc_str = ""
|
||||
|
||||
cent_cachefile = '%s/%scent_%s_%s%s.npy' % (
|
||||
cacheroot, prefix, dbname, preproc_str, ivf_str)
|
||||
cent_cachefile = "%s/%scent_%s_%s%s.npy" % (
|
||||
cacheroot,
|
||||
prefix,
|
||||
dbname,
|
||||
preproc_str,
|
||||
ivf_str,
|
||||
)
|
||||
|
||||
index_cachefile = '%s/%s%s_%s%s,%s.index' % (
|
||||
cacheroot, prefix, dbname, preproc_str, ivf_str, pqflat_str)
|
||||
index_cachefile = "%s/%s%s_%s%s,%s.index" % (
|
||||
cacheroot,
|
||||
prefix,
|
||||
dbname,
|
||||
preproc_str,
|
||||
ivf_str,
|
||||
pqflat_str,
|
||||
)
|
||||
|
||||
|
||||
if not use_cache:
|
||||
@@ -328,7 +349,7 @@ for _ in range(ngpu):
|
||||
|
||||
|
||||
def make_vres_vdev(i0=0, i1=-1):
|
||||
" return vectors of device ids and resources useful for gpu_multiple"
|
||||
"return vectors of device ids and resources useful for gpu_multiple"
|
||||
vres = faiss.GpuResourcesVector()
|
||||
vdev = faiss.IntVector()
|
||||
if i1 == -1:
|
||||
@@ -348,33 +369,31 @@ def compute_GT():
|
||||
print("compute GT")
|
||||
t0 = time.time()
|
||||
|
||||
gt_I = np.zeros((nq_gt, gt_sl), dtype='int64')
|
||||
gt_D = np.zeros((nq_gt, gt_sl), dtype='float32')
|
||||
gt_I = np.zeros((nq_gt, gt_sl), dtype="int64")
|
||||
gt_D = np.zeros((nq_gt, gt_sl), dtype="float32")
|
||||
heaps = faiss.float_maxheap_array_t()
|
||||
heaps.k = gt_sl
|
||||
heaps.nh = nq_gt
|
||||
heaps.val = faiss.swig_ptr(gt_D)
|
||||
heaps.ids = faiss.swig_ptr(gt_I)
|
||||
heaps.heapify()
|
||||
bs = 10 ** 5
|
||||
bs = 10**5
|
||||
|
||||
n, d = xb.shape
|
||||
xqs = sanitize(xq[:nq_gt])
|
||||
|
||||
db_gt = faiss.IndexFlatL2(d)
|
||||
vres, vdev = make_vres_vdev()
|
||||
db_gt_gpu = faiss.index_cpu_to_gpu_multiple(
|
||||
vres, vdev, db_gt)
|
||||
db_gt_gpu = faiss.index_cpu_to_gpu_multiple(vres, vdev, db_gt)
|
||||
|
||||
# compute ground-truth by blocks of bs, and add to heaps
|
||||
for i0, xsl in dataset_iterator(xb, IdentPreproc(d), bs):
|
||||
db_gt_gpu.add(xsl)
|
||||
D, I = db_gt_gpu.search(xqs, gt_sl)
|
||||
I += i0
|
||||
heaps.addn_with_ids(
|
||||
gt_sl, faiss.swig_ptr(D), faiss.swig_ptr(I), gt_sl)
|
||||
heaps.addn_with_ids(gt_sl, faiss.swig_ptr(D), faiss.swig_ptr(I), gt_sl)
|
||||
db_gt_gpu.reset()
|
||||
print("\r %d/%d, %.3f s" % (i0, n, time.time() - t0), end=' ')
|
||||
print("\r %d/%d, %.3f s" % (i0, n, time.time() - t0), end=" ")
|
||||
print()
|
||||
heaps.reorder()
|
||||
|
||||
@@ -402,12 +421,12 @@ def train_preprocessor():
|
||||
print("train preproc", preproc_str)
|
||||
d = xt.shape[1]
|
||||
t0 = time.time()
|
||||
if preproc_str.startswith('OPQ'):
|
||||
fi = preproc_str[3:-1].split('_')
|
||||
if preproc_str.startswith("OPQ"):
|
||||
fi = preproc_str[3:-1].split("_")
|
||||
m = int(fi[0])
|
||||
dout = int(fi[1]) if len(fi) == 2 else d
|
||||
preproc = faiss.OPQMatrix(d, m, dout)
|
||||
elif preproc_str.startswith('PCAR'):
|
||||
elif preproc_str.startswith("PCAR"):
|
||||
dout = int(preproc_str[4:-1])
|
||||
preproc = faiss.PCAMatrix(d, dout, 0, True)
|
||||
else:
|
||||
@@ -445,15 +464,13 @@ def train_coarse_quantizer(x, k, preproc):
|
||||
# clus.niter = 2
|
||||
clus.max_points_per_centroid = 10000000
|
||||
|
||||
print("apply preproc on shape", x.shape, 'k=', k)
|
||||
print("apply preproc on shape", x.shape, "k=", k)
|
||||
t0 = time.time()
|
||||
x = preproc.apply_py(sanitize(x))
|
||||
print(" preproc %.3f s output shape %s" % (
|
||||
time.time() - t0, x.shape))
|
||||
print(" preproc %.3f s output shape %s" % (time.time() - t0, x.shape))
|
||||
|
||||
vres, vdev = make_vres_vdev()
|
||||
index = faiss.index_cpu_to_gpu_multiple(
|
||||
vres, vdev, faiss.IndexFlatL2(d))
|
||||
index = faiss.index_cpu_to_gpu_multiple(vres, vdev, faiss.IndexFlatL2(d))
|
||||
|
||||
clus.train(x, index)
|
||||
centroids = faiss.vector_float_to_array(clus.centroids)
|
||||
@@ -491,10 +508,11 @@ def prepare_trained_index(preproc):
|
||||
|
||||
coarse_quantizer = prepare_coarse_quantizer(preproc)
|
||||
d = preproc.d_out
|
||||
if pqflat_str == 'Flat':
|
||||
if pqflat_str == "Flat":
|
||||
print("making an IVFFlat index")
|
||||
idx_model = faiss.IndexIVFFlat(coarse_quantizer, d, ncent,
|
||||
faiss.METRIC_L2)
|
||||
idx_model = faiss.IndexIVFFlat(
|
||||
coarse_quantizer, d, ncent, faiss.METRIC_L2
|
||||
)
|
||||
else:
|
||||
m = int(pqflat_str[2:])
|
||||
assert m < 56 or use_float16, "PQ%d will work only with -float16" % m
|
||||
@@ -516,7 +534,7 @@ def prepare_trained_index(preproc):
|
||||
|
||||
def compute_populated_index(preproc):
|
||||
"""Add elements to a sharded index. Return the index and if available
|
||||
a sharded gpu_index that contains the same data. """
|
||||
a sharded gpu_index that contains the same data."""
|
||||
|
||||
indexall = prepare_trained_index(preproc)
|
||||
|
||||
@@ -530,8 +548,7 @@ def compute_populated_index(preproc):
|
||||
co.shard = True
|
||||
assert co.shard_type in (0, 1, 2)
|
||||
vres, vdev = make_vres_vdev()
|
||||
gpu_index = faiss.index_cpu_to_gpu_multiple(
|
||||
vres, vdev, indexall, co)
|
||||
gpu_index = faiss.index_cpu_to_gpu_multiple(vres, vdev, indexall, co)
|
||||
|
||||
print("add...")
|
||||
t0 = time.time()
|
||||
@@ -550,15 +567,14 @@ def compute_populated_index(preproc):
|
||||
index_src_gpu.reserveMemory(max_add)
|
||||
gpu_index.sync_with_shard_indexes()
|
||||
|
||||
print('\r%d/%d (%.3f s) ' % (
|
||||
i0, nb, time.time() - t0), end=' ')
|
||||
print("\r%d/%d (%.3f s) " % (i0, nb, time.time() - t0), end=" ")
|
||||
sys.stdout.flush()
|
||||
print("Add time: %.3f s" % (time.time() - t0))
|
||||
|
||||
print("Aggregate indexes to CPU")
|
||||
t0 = time.time()
|
||||
|
||||
if hasattr(gpu_index, 'at'):
|
||||
if hasattr(gpu_index, "at"):
|
||||
# it is a sharded index
|
||||
for i in range(ngpu):
|
||||
index_src = faiss.index_gpu_to_cpu(gpu_index.at(i))
|
||||
@@ -591,7 +607,8 @@ def compute_populated_index_2(preproc):
|
||||
|
||||
vres, vdev = make_vres_vdev()
|
||||
coarse_quantizer_gpu = faiss.index_cpu_to_gpu_multiple(
|
||||
vres, vdev, indexall.quantizer)
|
||||
vres, vdev, indexall.quantizer
|
||||
)
|
||||
|
||||
def quantize(args):
|
||||
(i0, xs) = args
|
||||
@@ -607,16 +624,17 @@ def compute_populated_index_2(preproc):
|
||||
for i0, xs, assign in stage2:
|
||||
i1 = i0 + xs.shape[0]
|
||||
if indexall.__class__ == faiss.IndexIVFPQ:
|
||||
indexall.add_core_o(i1 - i0, faiss.swig_ptr(xs),
|
||||
None, None, faiss.swig_ptr(assign))
|
||||
indexall.add_core_o(
|
||||
i1 - i0, faiss.swig_ptr(xs), None, None, faiss.swig_ptr(assign)
|
||||
)
|
||||
elif indexall.__class__ == faiss.IndexIVFFlat:
|
||||
indexall.add_core(i1 - i0, faiss.swig_ptr(xs), None,
|
||||
faiss.swig_ptr(assign))
|
||||
indexall.add_core(
|
||||
i1 - i0, faiss.swig_ptr(xs), None, faiss.swig_ptr(assign)
|
||||
)
|
||||
else:
|
||||
assert False
|
||||
|
||||
print('\r%d/%d (%.3f s) ' % (
|
||||
i0, nb, time.time() - t0), end=' ')
|
||||
print("\r%d/%d (%.3f s) " % (i0, nb, time.time() - t0), end=" ")
|
||||
sys.stdout.flush()
|
||||
print("Add time: %.3f s" % (time.time() - t0))
|
||||
|
||||
@@ -644,7 +662,7 @@ def get_populated_index(preproc):
|
||||
co.usePrecomputed = use_precomputed_tables
|
||||
co.indicesOptions = 0
|
||||
co.verbose = True
|
||||
co.shard = True # the replicas will be made "manually"
|
||||
co.shard = True # the replicas will be made "manually"
|
||||
t0 = time.time()
|
||||
print("CPU index contains %d vectors, move to GPU" % indexall.ntotal)
|
||||
if replicas == 1:
|
||||
@@ -652,8 +670,7 @@ def get_populated_index(preproc):
|
||||
if not gpu_index:
|
||||
print("copying loaded index to GPUs")
|
||||
vres, vdev = make_vres_vdev()
|
||||
index = faiss.index_cpu_to_gpu_multiple(
|
||||
vres, vdev, indexall, co)
|
||||
index = faiss.index_cpu_to_gpu_multiple(vres, vdev, indexall, co)
|
||||
else:
|
||||
index = gpu_index
|
||||
|
||||
@@ -671,8 +688,7 @@ def get_populated_index(preproc):
|
||||
|
||||
print(" dispatch to GPUs %d:%d" % (gpu0, gpu1))
|
||||
|
||||
index1 = faiss.index_cpu_to_gpu_multiple(
|
||||
vres, vdev, indexall, co)
|
||||
index1 = faiss.index_cpu_to_gpu_multiple(vres, vdev, indexall, co)
|
||||
index1.this.disown()
|
||||
index.addIndex(index1)
|
||||
index.own_fields = True
|
||||
@@ -681,7 +697,6 @@ def get_populated_index(preproc):
|
||||
return index
|
||||
|
||||
|
||||
|
||||
#################################################################
|
||||
# Perform search
|
||||
#################################################################
|
||||
@@ -697,20 +712,23 @@ def eval_dataset(index, preproc):
|
||||
sl = query_batch_size
|
||||
nq = xq.shape[0]
|
||||
for nprobe in nprobes:
|
||||
ps.set_index_parameter(index, 'nprobe', nprobe)
|
||||
ps.set_index_parameter(index, "nprobe", nprobe)
|
||||
t0 = time.time()
|
||||
|
||||
if sl == 0:
|
||||
D, I = index.search(preproc.apply_py(sanitize(xq)), nnn)
|
||||
else:
|
||||
I = np.empty((nq, nnn), dtype='int32')
|
||||
D = np.empty((nq, nnn), dtype='float32')
|
||||
I = np.empty((nq, nnn), dtype="int32")
|
||||
D = np.empty((nq, nnn), dtype="float32")
|
||||
|
||||
inter_res = ''
|
||||
inter_res = ""
|
||||
|
||||
for i0, xs in dataset_iterator(xq, preproc, sl):
|
||||
print('\r%d/%d (%.3f s%s) ' % (
|
||||
i0, nq, time.time() - t0, inter_res), end=' ')
|
||||
print(
|
||||
"\r%d/%d (%.3f s%s) "
|
||||
% (i0, nq, time.time() - t0, inter_res),
|
||||
end=" ",
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
||||
i1 = i0 + xs.shape[0]
|
||||
@@ -720,24 +738,25 @@ def eval_dataset(index, preproc):
|
||||
D[i0:i1] = Di
|
||||
|
||||
if knngraph and not inter_res and i1 >= nq_gt:
|
||||
ires = eval_intersection_measure(
|
||||
gt_I[:, :nnn], I[:nq_gt])
|
||||
inter_res = ', %.4f' % ires
|
||||
ires = eval_intersection_measure(gt_I[:, :nnn], I[:nq_gt])
|
||||
inter_res = ", %.4f" % ires
|
||||
|
||||
t1 = time.time()
|
||||
if knngraph:
|
||||
ires = eval_intersection_measure(gt_I[:, :nnn], I[:nq_gt])
|
||||
print(" probe=%-3d: %.3f s rank-%d intersection results: %.4f" % (
|
||||
nprobe, t1 - t0, nnn, ires))
|
||||
print(
|
||||
" probe=%-3d: %.3f s rank-%d intersection results: %.4f"
|
||||
% (nprobe, t1 - t0, nnn, ires)
|
||||
)
|
||||
else:
|
||||
print(" probe=%-3d: %.3f s" % (nprobe, t1 - t0), end=' ')
|
||||
print(" probe=%-3d: %.3f s" % (nprobe, t1 - t0), end=" ")
|
||||
gtc = gt_I[:, :1]
|
||||
nq = xq.shape[0]
|
||||
for rank in (1, 10, 100):
|
||||
if rank > nnn:
|
||||
continue
|
||||
nok = (I[:, :rank] == gtc).sum()
|
||||
print("1-R@%d: %.4f" % (rank, nok / float(nq)), end=' ')
|
||||
print("1-R@%d: %.4f" % (rank, nok / float(nq)), end=" ")
|
||||
print()
|
||||
if I_fname:
|
||||
I_fname_i = I_fname % nprobe
|
||||
|
||||
@@ -84,4 +84,7 @@ for lnprobe in range(10):
|
||||
index.nprobe = nprobe
|
||||
t, r = evaluate(index, xq, gt, 100)
|
||||
|
||||
print("nprobe=%4d %.3f ms recalls= %.4f %.4f %.4f" % (nprobe, t, r[1], r[10], r[100]))
|
||||
print(
|
||||
"nprobe=%4d %.3f ms recalls= %.4f %.4f %.4f"
|
||||
% (nprobe, t, r[1], r[10], r[100])
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ if __name__ == "__main__":
|
||||
for d in 4, 8, 16, 32, 64, 128, 256, 512, 1024:
|
||||
nq = 10000
|
||||
nb = 30000
|
||||
print('Bits per vector = 8 *', d)
|
||||
xq = faiss.randint((nq, d // 4), seed=1234, vmax=256**4).view('uint8')
|
||||
xb = faiss.randint((nb, d // 4), seed=1234, vmax=256**4).view('uint8')
|
||||
print("Bits per vector = 8 *", d)
|
||||
xq = faiss.randint((nq, d // 4), seed=1234, vmax=256**4).view("uint8")
|
||||
xb = faiss.randint((nb, d // 4), seed=1234, vmax=256**4).view("uint8")
|
||||
for variant in "hc", "mc":
|
||||
print(f"{variant=:}", end="\t")
|
||||
for k in 1, 4, 16, 64, 256:
|
||||
@@ -25,5 +25,9 @@ if __name__ == "__main__":
|
||||
D, I = faiss.knn_hamming(xq, xb, k, variant=variant)
|
||||
t1 = time.time()
|
||||
times.append(t1 - t0)
|
||||
print(f'| {k=:} t={np.mean(times):.3f} s ± {np.std(times):.3f} ', flush=True, end="")
|
||||
print(
|
||||
f"| {k=:} t={np.mean(times):.3f} s ± {np.std(times):.3f} ",
|
||||
flush=True,
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
|
||||
@@ -221,7 +221,7 @@ if "hnsw_locks" in todo:
|
||||
t2 = None
|
||||
for i in range(0, len(xb), batch_size):
|
||||
t1 = time.time()
|
||||
index.add(xb[i: i + batch_size])
|
||||
index.add(xb[i : i + batch_size])
|
||||
t2 = time.time()
|
||||
if i > 2 and t2 - t0 > 2:
|
||||
break
|
||||
|
||||
@@ -104,7 +104,9 @@ def benchmark_dataset(ds, dataset_name, k=10, nlevels=8, M=32):
|
||||
plot_data = []
|
||||
|
||||
# HNSW Flat (baseline)
|
||||
eval_and_plot(f"HNSW{M},Flat", ds, k=k, nlevels=nlevels, plot_data=plot_data)
|
||||
eval_and_plot(
|
||||
f"HNSW{M},Flat", ds, k=k, nlevels=nlevels, plot_data=plot_data
|
||||
)
|
||||
|
||||
# HNSW Flat Panorama (with PCA to concentrate energy)
|
||||
eval_and_plot(
|
||||
@@ -153,6 +155,6 @@ if __name__ == "__main__":
|
||||
print(f"{'='*60}")
|
||||
benchmark_dataset(ds, name, k=k, nlevels=nlevels, M=M)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("\n" + "=" * 60)
|
||||
print("All benchmarks completed!")
|
||||
print("="*60)
|
||||
print("=" * 60)
|
||||
|
||||
@@ -108,7 +108,8 @@ def run_benchmark(
|
||||
for headroom in headroom_values:
|
||||
for rep in range(reps):
|
||||
index, build_time = build_hnsw_index(
|
||||
d, m, xb, ef_construction, headroom)
|
||||
d, m, xb, ef_construction, headroom
|
||||
)
|
||||
results["build_times"][headroom] = build_time
|
||||
|
||||
faiss.cvar.hnsw_stats.reset()
|
||||
|
||||
+174
-128
@@ -34,12 +34,13 @@ def search_preassigned(xq, k, index, quantizer, batch_size=0):
|
||||
nprobe = index.nprobe
|
||||
if batch_size == 0:
|
||||
batch_size = n + 1
|
||||
D = np.empty((n, k), dtype='float32')
|
||||
I = np.empty((n, k), dtype='int64')
|
||||
D = np.empty((n, k), dtype="float32")
|
||||
I = np.empty((n, k), dtype="int64")
|
||||
for i0 in range(0, n, batch_size):
|
||||
Dq, Iq = quantizer.search(xq[i0:i0 + batch_size], nprobe)
|
||||
D[i0:i0 + batch_size], I[i0:i0 + batch_size] = \
|
||||
index.search_preassigned(xq[i0:i0 + batch_size], k, Iq, Dq)
|
||||
Dq, Iq = quantizer.search(xq[i0 : i0 + batch_size], nprobe)
|
||||
D[i0 : i0 + batch_size], I[i0 : i0 + batch_size] = (
|
||||
index.search_preassigned(xq[i0 : i0 + batch_size], k, Iq, Dq)
|
||||
)
|
||||
return D, I
|
||||
|
||||
|
||||
@@ -61,16 +62,17 @@ def tiled_search_preassigned(xq, k, index, quantizer, batch_size=32768):
|
||||
i1 = min(i0 + batch_size, n)
|
||||
return quantizer.search(xq[i0:i1], nprobe)
|
||||
|
||||
D = np.empty((n, k), dtype='float32')
|
||||
I = np.empty((n, k), dtype='int64')
|
||||
D = np.empty((n, k), dtype="float32")
|
||||
I = np.empty((n, k), dtype="int64")
|
||||
qq = coarse_quant(0)
|
||||
|
||||
for i0 in range(0, n, batch_size):
|
||||
i1 = min(i0 + batch_size, n)
|
||||
qq_next = qq_pool.apply_async(coarse_quant, (i0 + batch_size, ))
|
||||
qq_next = qq_pool.apply_async(coarse_quant, (i0 + batch_size,))
|
||||
Dq, Iq = qq
|
||||
index.search_preassigned(
|
||||
xq[i0:i1], k, Iq=Iq, Dq=Dq, I=I[i0:i1], D=D[i0:i1])
|
||||
xq[i0:i1], k, Iq=Iq, Dq=Dq, I=I[i0:i1], D=D[i0:i1]
|
||||
)
|
||||
qq = qq_next.get()
|
||||
|
||||
qq_pool.close()
|
||||
@@ -81,6 +83,7 @@ def tiled_search_preassigned(xq, k, index, quantizer, batch_size=32768):
|
||||
# IVF index objects with a separate coarse quantizer
|
||||
#################################################################
|
||||
|
||||
|
||||
class SeparateCoarseQuantizationIndex:
|
||||
"""
|
||||
Separately manage the coarse quantizer and the IVF index.
|
||||
@@ -120,10 +123,12 @@ class SeparateCoarseQuantizationIndex:
|
||||
return self.index_ivf.search_preassigned(xq, k, Iq, Dq)
|
||||
if self.seq_tiling:
|
||||
return search_preassigned(
|
||||
xq, k, self.index_ivf, self.quantizer, self.bs)
|
||||
xq, k, self.index_ivf, self.quantizer, self.bs
|
||||
)
|
||||
else:
|
||||
return tiled_search_preassigned(
|
||||
xq, k, self.index_ivf, self.quantizer, self.bs)
|
||||
xq, k, self.index_ivf, self.quantizer, self.bs
|
||||
)
|
||||
|
||||
|
||||
class ShardedGPUIndex:
|
||||
@@ -131,6 +136,7 @@ class ShardedGPUIndex:
|
||||
Multiple GPU indexes, each on its GPU, with a common coarse quantizer.
|
||||
The Python version of IndexShardsIVF
|
||||
"""
|
||||
|
||||
def __init__(self, quantizer, index, bs=-1, seq_tiling=False):
|
||||
self.quantizer = quantizer
|
||||
self.cpu_index = index
|
||||
@@ -159,8 +165,8 @@ class ShardedGPUIndex:
|
||||
sub_index_0 = faiss.downcast_index(index.at(0))
|
||||
nprobe = sub_index_0.nprobe
|
||||
|
||||
Dall = np.empty((ngpu, nq, k), dtype='float32')
|
||||
Iall = np.empty((ngpu, nq, k), dtype='int64')
|
||||
Dall = np.empty((ngpu, nq, k), dtype="float32")
|
||||
Iall = np.empty((ngpu, nq, k), dtype="int64")
|
||||
bs = self.bs
|
||||
if bs <= 0:
|
||||
|
||||
@@ -169,7 +175,9 @@ class ShardedGPUIndex:
|
||||
def do_search(rank):
|
||||
gpu_index = faiss.downcast_index(index.at(rank))
|
||||
Dall[rank], Iall[rank] = gpu_index.search_preassigned(
|
||||
xq, k, Iq, Dq)
|
||||
xq, k, Iq, Dq
|
||||
)
|
||||
|
||||
list(self.pool.map(do_search, range(ngpu)))
|
||||
else:
|
||||
qq_pool = self.q_pool
|
||||
@@ -178,30 +186,32 @@ class ShardedGPUIndex:
|
||||
def coarse_quant(i0):
|
||||
if i0 >= nq:
|
||||
return None
|
||||
return self.quantizer.search(xq[i0:i0 + bs], nprobe)
|
||||
return self.quantizer.search(xq[i0 : i0 + bs], nprobe)
|
||||
|
||||
def do_search(rank, i0, qq):
|
||||
gpu_index = faiss.downcast_index(index.at(rank))
|
||||
Dq, Iq = qq
|
||||
Dall[rank, i0:i0 + bs], Iall[rank, i0:i0 + bs] = \
|
||||
gpu_index.search_preassigned(xq[i0:i0 + bs], k, Iq, Dq)
|
||||
Dall[rank, i0 : i0 + bs], Iall[rank, i0 : i0 + bs] = (
|
||||
gpu_index.search_preassigned(xq[i0 : i0 + bs], k, Iq, Dq)
|
||||
)
|
||||
|
||||
qq = coarse_quant(0)
|
||||
|
||||
for i0 in range(0, nq, bs):
|
||||
qq_next = qq_pool.apply_async(coarse_quant, (i0 + bs, ))
|
||||
list(self.pool.map(
|
||||
lambda rank: do_search(rank, i0, qq),
|
||||
range(ngpu)
|
||||
))
|
||||
qq_next = qq_pool.apply_async(coarse_quant, (i0 + bs,))
|
||||
list(
|
||||
self.pool.map(
|
||||
lambda rank: do_search(rank, i0, qq), range(ngpu)
|
||||
)
|
||||
)
|
||||
qq = qq_next.get()
|
||||
|
||||
return faiss.merge_knn_results(Dall, Iall)
|
||||
|
||||
|
||||
def extract_index_ivf(index):
|
||||
""" extract the IVF sub-index from the index, supporting GpuIndexes
|
||||
as well """
|
||||
"""extract the IVF sub-index from the index, supporting GpuIndexes
|
||||
as well"""
|
||||
try:
|
||||
return faiss.extract_index_ivf(index)
|
||||
except RuntimeError:
|
||||
@@ -221,7 +231,8 @@ def set_index_parameter(index, name, val):
|
||||
set_index_parameter(index.index_ivf, name, val)
|
||||
elif name.startswith("quantizer_"):
|
||||
set_index_parameter(
|
||||
index.quantizer, name[name.find("_") + 1:], val)
|
||||
index.quantizer, name[name.find("_") + 1 :], val
|
||||
)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
return
|
||||
@@ -231,7 +242,8 @@ def set_index_parameter(index, name, val):
|
||||
set_index_parameter(index.cpu_index, name, val)
|
||||
elif name.startswith("quantizer_"):
|
||||
set_index_parameter(
|
||||
index.quantizer, name[name.find("_") + 1:], val)
|
||||
index.quantizer, name[name.find("_") + 1 :], val
|
||||
)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
return
|
||||
@@ -244,20 +256,23 @@ def set_index_parameter(index, name, val):
|
||||
elif isinstance(index, faiss.IndexShardsIVF):
|
||||
if name != "nprobe" and name.startswith("quantizer_"):
|
||||
set_index_parameter(
|
||||
index.quantizer, name[name.find("_") + 1:], val)
|
||||
index.quantizer, name[name.find("_") + 1 :], val
|
||||
)
|
||||
else:
|
||||
for i in range(index.count()):
|
||||
sub_index = index.at(i)
|
||||
set_index_parameter(sub_index, name, val)
|
||||
elif (isinstance(index, faiss.IndexShards) or
|
||||
isinstance(index, faiss.IndexReplicas)):
|
||||
elif isinstance(index, faiss.IndexShards) or isinstance(
|
||||
index, faiss.IndexReplicas
|
||||
):
|
||||
for i in range(index.count()):
|
||||
sub_index = index.at(i)
|
||||
set_index_parameter(sub_index, name, val)
|
||||
elif name.startswith("quantizer_"):
|
||||
index_ivf = extract_index_ivf(index)
|
||||
set_index_parameter(
|
||||
index_ivf.quantizer, name[name.find("_") + 1:], val)
|
||||
index_ivf.quantizer, name[name.find("_") + 1 :], val
|
||||
)
|
||||
elif name == "efSearch":
|
||||
index.hnsw.efSearch
|
||||
index.hnsw.efSearch = int(val)
|
||||
@@ -280,47 +295,85 @@ def main():
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('dataset options')
|
||||
aa('--nq', type=int, default=int(10e5),
|
||||
help="nb queries (queries will be duplicated if below that number)")
|
||||
aa('--db', default='bigann10M', help='dataset')
|
||||
group = parser.add_argument_group("dataset options")
|
||||
aa(
|
||||
"--nq",
|
||||
type=int,
|
||||
default=int(10e5),
|
||||
help="nb queries (queries will be duplicated if below that number)",
|
||||
)
|
||||
aa("--db", default="bigann10M", help="dataset")
|
||||
|
||||
group = parser.add_argument_group('index options')
|
||||
aa('--indexname', default="", help="override index name")
|
||||
aa('--mmap', default=False, action='store_true', help='mmap index')
|
||||
aa('--shard_type', default=1, type=int, help="set type of sharding")
|
||||
aa('--useFloat16', default=False, action='store_true',
|
||||
help='GPU cloner options')
|
||||
aa('--useFloat16CoarseQuantizer', default=False, action='store_true',
|
||||
help='GPU cloner options')
|
||||
aa('--usePrecomputed', default=False, action='store_true',
|
||||
help='GPU cloner options')
|
||||
group = parser.add_argument_group('search options')
|
||||
aa('--k', type=int, default=100)
|
||||
aa('--search_type', default="cpu",
|
||||
group = parser.add_argument_group("index options")
|
||||
aa("--indexname", default="", help="override index name")
|
||||
aa("--mmap", default=False, action="store_true", help="mmap index")
|
||||
aa("--shard_type", default=1, type=int, help="set type of sharding")
|
||||
aa(
|
||||
"--useFloat16",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="GPU cloner options",
|
||||
)
|
||||
aa(
|
||||
"--useFloat16CoarseQuantizer",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="GPU cloner options",
|
||||
)
|
||||
aa(
|
||||
"--usePrecomputed",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="GPU cloner options",
|
||||
)
|
||||
group = parser.add_argument_group("search options")
|
||||
aa("--k", type=int, default=100)
|
||||
aa(
|
||||
"--search_type",
|
||||
default="cpu",
|
||||
choices=[
|
||||
"cpu", "gpu", "gpu_flat_quantizer",
|
||||
"cpu_flat_gpu_quantizer", "gpu_tiled", "gpu_ivf_quantizer",
|
||||
"multi_gpu", "multi_gpu_flat_quantizer",
|
||||
"multi_gpu_sharded", "multi_gpu_flat_quantizer_sharded",
|
||||
"multi_gpu_sharded1", "multi_gpu_sharded1_flat",
|
||||
"cpu",
|
||||
"gpu",
|
||||
"gpu_flat_quantizer",
|
||||
"cpu_flat_gpu_quantizer",
|
||||
"gpu_tiled",
|
||||
"gpu_ivf_quantizer",
|
||||
"multi_gpu",
|
||||
"multi_gpu_flat_quantizer",
|
||||
"multi_gpu_sharded",
|
||||
"multi_gpu_flat_quantizer_sharded",
|
||||
"multi_gpu_sharded1",
|
||||
"multi_gpu_sharded1_flat",
|
||||
"multi_gpu_sharded1_ivf",
|
||||
"multi_gpu_Csharded1", "multi_gpu_Csharded1_flat",
|
||||
"multi_gpu_Csharded1",
|
||||
"multi_gpu_Csharded1_flat",
|
||||
"multi_gpu_Csharded1_ivf",
|
||||
],
|
||||
help="how to search"
|
||||
help="how to search",
|
||||
)
|
||||
aa('--ivf_quant_nlist', type=int, default=1024,
|
||||
help="nb of inverted lists for IVF quantizer")
|
||||
aa('--batch_size', type=int, default=-1,
|
||||
help="batch size for tiled CPU / GPU computation (-1= no tiling)")
|
||||
aa('--n_autotune', type=int, default=300,
|
||||
help="max nb of auto-tuning steps")
|
||||
aa('--nt', type=int, default=-1, help="force number of CPU threads to this")
|
||||
aa(
|
||||
"--ivf_quant_nlist",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="nb of inverted lists for IVF quantizer",
|
||||
)
|
||||
aa(
|
||||
"--batch_size",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="batch size for tiled CPU / GPU computation (-1= no tiling)",
|
||||
)
|
||||
aa(
|
||||
"--n_autotune",
|
||||
type=int,
|
||||
default=300,
|
||||
help="max nb of auto-tuning steps",
|
||||
)
|
||||
aa("--nt", type=int, default=-1, help="force number of CPU threads to this")
|
||||
|
||||
group = parser.add_argument_group('output options')
|
||||
aa('--quiet', default=False, action="store_true")
|
||||
aa('--stats', default="", help="pickle to store output stats")
|
||||
group = parser.add_argument_group("output options")
|
||||
aa("--quiet", default=False, action="store_true")
|
||||
aa("--stats", default="", help="pickle to store output stats")
|
||||
|
||||
args = parser.parse_args()
|
||||
print("args:", args)
|
||||
@@ -342,7 +395,7 @@ def main():
|
||||
xqx = []
|
||||
n = 0
|
||||
while n < args.nq:
|
||||
xqx.append(xq[:args.nq - n])
|
||||
xqx.append(xq[: args.nq - n])
|
||||
n += len(xqx[-1])
|
||||
print(f"increased nb queries from {len(xq)} to {n}")
|
||||
xq = np.vstack(xqx)
|
||||
@@ -365,10 +418,12 @@ def main():
|
||||
print("prepare index")
|
||||
op = OperatingPointsWithRanges()
|
||||
op.add_range(
|
||||
"nprobe", [
|
||||
2 ** i for i in range(20)
|
||||
if 2 ** i < index_ivf.nlist * 0.1 and 2 ** i <= 4096
|
||||
]
|
||||
"nprobe",
|
||||
[
|
||||
2**i
|
||||
for i in range(20)
|
||||
if 2**i < index_ivf.nlist * 0.1 and 2**i <= 4096
|
||||
],
|
||||
)
|
||||
|
||||
# prepare options for GPU clone
|
||||
@@ -380,18 +435,12 @@ def main():
|
||||
co.shard_type = args.shard_type
|
||||
|
||||
if args.search_type == "cpu":
|
||||
op.add_range(
|
||||
"quantizer_efSearch",
|
||||
[2 ** i for i in range(10)]
|
||||
)
|
||||
op.add_range("quantizer_efSearch", [2**i for i in range(10)])
|
||||
elif args.search_type == "gpu":
|
||||
print("move index to 1 GPU")
|
||||
res = faiss.StandardGpuResources()
|
||||
index = faiss.index_cpu_to_gpu(res, 0, index, co)
|
||||
op.add_range(
|
||||
"quantizer_efSearch",
|
||||
[2 ** i for i in range(10)]
|
||||
)
|
||||
op.add_range("quantizer_efSearch", [2**i for i in range(10)])
|
||||
op.restrict_range("nprobe", 2049)
|
||||
elif args.search_type == "gpu_tiled":
|
||||
print("move index to 1 GPU")
|
||||
@@ -399,31 +448,28 @@ def main():
|
||||
quantizer_hnsw = replace_ivf_quantizer(index_ivf, new_quantizer)
|
||||
res = faiss.StandardGpuResources()
|
||||
index = faiss.index_cpu_to_gpu(res, 0, index, co)
|
||||
op.add_range(
|
||||
"quantizer_efSearch",
|
||||
[2 ** i for i in range(10)]
|
||||
)
|
||||
op.add_range("quantizer_efSearch", [2**i for i in range(10)])
|
||||
op.restrict_range("nprobe", 2049)
|
||||
index = SeparateCoarseQuantizationIndex(
|
||||
quantizer_hnsw, index, bs=args.batch_size)
|
||||
quantizer_hnsw, index, bs=args.batch_size
|
||||
)
|
||||
elif args.search_type == "gpu_ivf_quantizer":
|
||||
index_ivf = faiss.extract_index_ivf(index)
|
||||
centroids = index_ivf.quantizer.reconstruct_n()
|
||||
replace_ivf_quantizer(index_ivf, faiss.IndexFlatL2(index_ivf.d))
|
||||
res = faiss.StandardGpuResources()
|
||||
new_quantizer = faiss.index_factory(
|
||||
index_ivf.d, f"IVF{args.ivf_quant_nlist},Flat")
|
||||
index_ivf.d, f"IVF{args.ivf_quant_nlist},Flat"
|
||||
)
|
||||
new_quantizer.train(centroids)
|
||||
new_quantizer.add(centroids)
|
||||
index = SeparateCoarseQuantizationIndex(
|
||||
faiss.index_cpu_to_gpu(res, 0, new_quantizer, co),
|
||||
faiss.index_cpu_to_gpu(res, 0, index, co),
|
||||
bs=args.batch_size, seq_tiling=True
|
||||
)
|
||||
op.add_range(
|
||||
"quantizer_nprobe",
|
||||
[2 ** i for i in range(9)]
|
||||
bs=args.batch_size,
|
||||
seq_tiling=True,
|
||||
)
|
||||
op.add_range("quantizer_nprobe", [2**i for i in range(9)])
|
||||
op.restrict_range("nprobe", 1025)
|
||||
elif args.search_type == "gpu_flat_quantizer":
|
||||
index_ivf = faiss.extract_index_ivf(index)
|
||||
@@ -438,27 +484,29 @@ def main():
|
||||
res = faiss.StandardGpuResources()
|
||||
quantizer = faiss.index_cpu_to_gpu(res, 0, quantizer, co)
|
||||
index = SeparateCoarseQuantizationIndex(
|
||||
quantizer, index, bs=args.batch_size)
|
||||
quantizer, index, bs=args.batch_size
|
||||
)
|
||||
op.restrict_range("nprobe", 2049)
|
||||
elif args.search_type in ("multi_gpu", "multi_gpu_sharded"):
|
||||
print(f"move index to {faiss.get_num_gpus()} GPU")
|
||||
co.shard = "sharded" in args.search_type
|
||||
index = faiss.index_cpu_to_all_gpus(index, co=co)
|
||||
op.add_range(
|
||||
"quantizer_efSearch",
|
||||
[2 ** i for i in range(10)]
|
||||
)
|
||||
op.add_range("quantizer_efSearch", [2**i for i in range(10)])
|
||||
op.restrict_range("nprobe", 2049)
|
||||
elif args.search_type in (
|
||||
"multi_gpu_flat_quantizer", "multi_gpu_flat_quantizer_sharded"):
|
||||
"multi_gpu_flat_quantizer",
|
||||
"multi_gpu_flat_quantizer_sharded",
|
||||
):
|
||||
index_ivf = faiss.extract_index_ivf(index)
|
||||
new_quantizer = faiss.IndexFlatL2(ds.d)
|
||||
replace_ivf_quantizer(index_ivf, new_quantizer)
|
||||
index = faiss.index_cpu_to_all_gpus(index, co=co)
|
||||
op.restrict_range("nprobe", 2049)
|
||||
elif args.search_type in (
|
||||
"multi_gpu_sharded1", "multi_gpu_sharded1_flat",
|
||||
"multi_gpu_sharded1_ivf"):
|
||||
"multi_gpu_sharded1",
|
||||
"multi_gpu_sharded1_flat",
|
||||
"multi_gpu_sharded1_ivf",
|
||||
):
|
||||
print(f"move index to {faiss.get_num_gpus()} GPU")
|
||||
new_quantizer = faiss.IndexFlatL2(index_ivf.d)
|
||||
hnsw_quantizer = replace_ivf_quantizer(index_ivf, new_quantizer)
|
||||
@@ -469,47 +517,43 @@ def main():
|
||||
index = faiss.index_cpu_to_gpu_multiple_py(res, index, co, gpus)
|
||||
op.restrict_range("nprobe", 2049)
|
||||
if args.search_type == "multi_gpu_sharded1":
|
||||
op.add_range(
|
||||
"quantizer_efSearch",
|
||||
[2 ** i for i in range(10)]
|
||||
)
|
||||
op.add_range("quantizer_efSearch", [2**i for i in range(10)])
|
||||
index = ShardedGPUIndex(hnsw_quantizer, index, bs=args.batch_size)
|
||||
elif args.search_type == "multi_gpu_sharded1_ivf":
|
||||
centroids = hnsw_quantizer.storage.reconstruct_n()
|
||||
quantizer = faiss.index_factory(
|
||||
centroids.shape[1], f"IVF{args.ivf_quant_nlist},Flat")
|
||||
centroids.shape[1], f"IVF{args.ivf_quant_nlist},Flat"
|
||||
)
|
||||
quantizer.train(centroids)
|
||||
quantizer.add(centroids)
|
||||
co.shard = False
|
||||
quantizer = faiss.index_cpu_to_gpu_multiple_py(
|
||||
res, quantizer, co, gpus)
|
||||
res, quantizer, co, gpus
|
||||
)
|
||||
index = ShardedGPUIndex(quantizer, index, bs=args.batch_size)
|
||||
|
||||
op.add_range(
|
||||
"quantizer_nprobe",
|
||||
[2 ** i for i in range(9)]
|
||||
)
|
||||
op.add_range("quantizer_nprobe", [2**i for i in range(9)])
|
||||
op.restrict_range("nprobe", 1025)
|
||||
elif args.search_type == "multi_gpu_sharded1_flat":
|
||||
quantizer = hnsw_quantizer.storage
|
||||
quantizer = faiss.index_cpu_to_gpu_multiple_py(
|
||||
res, quantizer, co, gpus)
|
||||
res, quantizer, co, gpus
|
||||
)
|
||||
index = ShardedGPUIndex(quantizer, index, bs=args.batch_size)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
elif args.search_type in (
|
||||
"multi_gpu_Csharded1", "multi_gpu_Csharded1_flat",
|
||||
"multi_gpu_Csharded1_ivf"):
|
||||
"multi_gpu_Csharded1",
|
||||
"multi_gpu_Csharded1_flat",
|
||||
"multi_gpu_Csharded1_ivf",
|
||||
):
|
||||
print(f"move index to {faiss.get_num_gpus()} GPU")
|
||||
co.shard = True
|
||||
co.common_ivf_quantizer
|
||||
co.common_ivf_quantizer = True
|
||||
op.restrict_range("nprobe", 2049)
|
||||
if args.search_type == "multi_gpu_Csharded1":
|
||||
op.add_range(
|
||||
"quantizer_efSearch",
|
||||
[2 ** i for i in range(10)]
|
||||
)
|
||||
op.add_range("quantizer_efSearch", [2**i for i in range(10)])
|
||||
index = faiss.index_cpu_to_all_gpus(index, co)
|
||||
elif args.search_type == "multi_gpu_Csharded1_flat":
|
||||
new_quantizer = faiss.IndexFlatL2(index_ivf.d)
|
||||
@@ -517,12 +561,10 @@ def main():
|
||||
index = faiss.index_cpu_to_all_gpus(index, co)
|
||||
elif args.search_type == "multi_gpu_Csharded1_ivf":
|
||||
quantizer = faiss.index_factory(
|
||||
index_ivf.d, f"IVF{args.ivf_quant_nlist},Flat")
|
||||
quantizer_hnsw = replace_ivf_quantizer(index_ivf, quantizer)
|
||||
op.add_range(
|
||||
"quantizer_nprobe",
|
||||
[2 ** i for i in range(9)]
|
||||
index_ivf.d, f"IVF{args.ivf_quant_nlist},Flat"
|
||||
)
|
||||
quantizer_hnsw = replace_ivf_quantizer(index_ivf, quantizer)
|
||||
op.add_range("quantizer_nprobe", [2**i for i in range(9)])
|
||||
index = faiss.index_cpu_to_all_gpus(index, co)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
@@ -543,7 +585,7 @@ def main():
|
||||
"processor": [l for l in open("/proc/cpuinfo") if "model name" in l][0],
|
||||
"GPU": list(os.popen("nvidia-smi", "r")),
|
||||
"mem": open("/proc/meminfo", "r").readlines(),
|
||||
"pid": os.getpid()
|
||||
"pid": os.getpid(),
|
||||
}
|
||||
op.args = args
|
||||
if args.stats:
|
||||
@@ -556,7 +598,9 @@ def main():
|
||||
|
||||
(max_perf, min_time) = op.predict_bounds(key)
|
||||
if not op.is_pareto_optimal(max_perf, min_time):
|
||||
print(f"SKIP, {max_perf=:.3f} {min_time=:.3f}", )
|
||||
print(
|
||||
f"SKIP, {max_perf=:.3f} {min_time=:.3f}",
|
||||
)
|
||||
continue
|
||||
|
||||
for name, val in parameters.items():
|
||||
@@ -577,19 +621,21 @@ def main():
|
||||
|
||||
recalls = {}
|
||||
for rank in 1, 10, 100:
|
||||
recall = (gt[:, :1] == I[:ds.nq, :rank]).sum() / ds.nq
|
||||
recall = (gt[:, :1] == I[: ds.nq, :rank]).sum() / ds.nq
|
||||
recalls[rank] = recall
|
||||
|
||||
print(f"time={t1 - t0:.3f} s recalls={recalls}")
|
||||
perf = recalls[1]
|
||||
op.add_operating_point(key, perf, t1 - t0)
|
||||
op.all_experiments.append({
|
||||
"cno": cno,
|
||||
"key": key,
|
||||
"parameters": parameters,
|
||||
"time": t1 - t0,
|
||||
"recalls": recalls
|
||||
})
|
||||
op.all_experiments.append(
|
||||
{
|
||||
"cno": cno,
|
||||
"key": key,
|
||||
"parameters": parameters,
|
||||
"time": t1 - t0,
|
||||
"recalls": recalls,
|
||||
}
|
||||
)
|
||||
|
||||
if args.stats:
|
||||
pickle.dump(op, open(args.stats, "wb"))
|
||||
|
||||
@@ -22,7 +22,7 @@ def format_tab(x):
|
||||
faiss.cvar.distance_compute_min_k_reservoir = 5
|
||||
|
||||
# for have_threads in True, False:
|
||||
for have_threads in False, :
|
||||
for have_threads in (False,):
|
||||
|
||||
if have_threads:
|
||||
# good config for Intel(R) Xeon(R) CPU E5-2698 v4 @ 2.20GHz
|
||||
@@ -70,17 +70,21 @@ for have_threads in False, :
|
||||
t0 = time.time()
|
||||
index.search(ds.get_queries(), k)
|
||||
t1 = time.time()
|
||||
if run >= nrun // 5: # the rest is considered warmup
|
||||
if run >= nrun // 5: # the rest is considered warmup
|
||||
times.append((t1 - t0))
|
||||
times = np.array(times)
|
||||
|
||||
if unit == "ms":
|
||||
times *= 1000
|
||||
print("search k=%3d t=%.3f ms (± %.4f)" % (
|
||||
k, np.mean(times), np.std(times)))
|
||||
print(
|
||||
"search k=%3d t=%.3f ms (± %.4f)"
|
||||
% (k, np.mean(times), np.std(times))
|
||||
)
|
||||
else:
|
||||
print("search k=%3d t=%.3f s (± %.4f)" % (
|
||||
k, np.mean(times), np.std(times)))
|
||||
print(
|
||||
"search k=%3d t=%.3f s (± %.4f)"
|
||||
% (k, np.mean(times), np.std(times))
|
||||
)
|
||||
restab1.append(np.mean(times))
|
||||
|
||||
print("restab=\n", format_tab(restab))
|
||||
|
||||
@@ -11,11 +11,9 @@ import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
try:
|
||||
from faiss.contrib.datasets_fb import \
|
||||
DatasetSIFT1M
|
||||
from faiss.contrib.datasets_fb import DatasetSIFT1M
|
||||
except ImportError:
|
||||
from faiss.contrib.datasets import \
|
||||
DatasetSIFT1M
|
||||
from faiss.contrib.datasets import DatasetSIFT1M
|
||||
|
||||
|
||||
# ds = DatasetDeep1B(10**6)
|
||||
@@ -46,8 +44,8 @@ def eval_recall(index, name):
|
||||
corrects = (gt == I).sum()
|
||||
recall = corrects / nq
|
||||
print(
|
||||
f'\tnprobe {index.nprobe:3d}, Recall@{k}: '
|
||||
f'{recall:.6f}, speed: {speed:.6f} ms/query'
|
||||
f"\tnprobe {index.nprobe:3d}, Recall@{k}: "
|
||||
f"{recall:.6f}, speed: {speed:.6f} ms/query"
|
||||
)
|
||||
|
||||
return recall, qps
|
||||
@@ -66,7 +64,7 @@ def eval_and_plot(name, rescale_norm=True, plot=True):
|
||||
faiss.write_index(index, index_path)
|
||||
|
||||
# search params
|
||||
if hasattr(index, 'rescale_norm'):
|
||||
if hasattr(index, "rescale_norm"):
|
||||
index.rescale_norm = rescale_norm
|
||||
name += f"(rescale_norm={rescale_norm})"
|
||||
faiss.omp_set_num_threads(1)
|
||||
@@ -108,5 +106,5 @@ eval_and_plot(f"IVF{nlist},RQ{M-2}x4fs_Nrq2x4")
|
||||
plt.title("Indices on SIFT1M")
|
||||
plt.xlabel("Recall@1")
|
||||
plt.ylabel("QPS")
|
||||
plt.legend(bbox_to_anchor=(1.02, 0.1), loc='upper left', borderaxespad=0)
|
||||
plt.savefig("bench_ivf_fastscan.png", bbox_inches='tight')
|
||||
plt.legend(bbox_to_anchor=(1.02, 0.1), loc="upper left", borderaxespad=0)
|
||||
plt.savefig("bench_ivf_fastscan.png", bbox_inches="tight")
|
||||
|
||||
@@ -30,7 +30,7 @@ nb, d = xb.shape
|
||||
nq, d = xq.shape
|
||||
nt, d = xt.shape
|
||||
|
||||
print('the dimension is {}, {}'.format(nb, d))
|
||||
print("the dimension is {}, {}".format(nb, d))
|
||||
|
||||
k = 64
|
||||
|
||||
@@ -43,7 +43,7 @@ def eval_recall(index, name, single_query=False):
|
||||
if single_query:
|
||||
t0 = time.time()
|
||||
for row in range(nq):
|
||||
Ds, Is = index.search(xq[row:row + 1], k=k)
|
||||
Ds, Is = index.search(xq[row : row + 1], k=k)
|
||||
D[row, :] = Ds
|
||||
I[row, :] = Is
|
||||
t = time.time() - t0
|
||||
@@ -53,16 +53,21 @@ def eval_recall(index, name, single_query=False):
|
||||
corrects = (gt[:, :1] == I[:, :k]).sum()
|
||||
recall = corrects / nq
|
||||
print(
|
||||
f'\tnprobe {index.nprobe:3d}, 1Recall@{k}: '
|
||||
f'{recall:.6f}, speed: {speed:.6f} ms/query'
|
||||
f"\tnprobe {index.nprobe:3d}, 1Recall@{k}: "
|
||||
f"{recall:.6f}, speed: {speed:.6f} ms/query"
|
||||
)
|
||||
|
||||
return recall, qps
|
||||
|
||||
|
||||
def eval_and_plot(
|
||||
name, rescale_norm=True, plot=True, single_query=False,
|
||||
implem=None, num_threads=1):
|
||||
name,
|
||||
rescale_norm=True,
|
||||
plot=True,
|
||||
single_query=False,
|
||||
implem=None,
|
||||
num_threads=1,
|
||||
):
|
||||
index = faiss.index_factory(d, name)
|
||||
index_path = f"indices/{name}.faissindex"
|
||||
|
||||
@@ -75,10 +80,10 @@ def eval_and_plot(
|
||||
faiss.write_index(index, index_path)
|
||||
|
||||
# search params
|
||||
if hasattr(index, 'rescale_norm'):
|
||||
if hasattr(index, "rescale_norm"):
|
||||
index.rescale_norm = rescale_norm
|
||||
name += f"(rescale_norm={rescale_norm})"
|
||||
if implem is not None and hasattr(index, 'implem'):
|
||||
if implem is not None and hasattr(index, "implem"):
|
||||
index.implem = implem
|
||||
name += f"(implem={implem})"
|
||||
if single_query:
|
||||
@@ -109,12 +114,18 @@ M, nlist = 64, 4096
|
||||
plt.figure(figsize=(8, 6), dpi=80)
|
||||
|
||||
eval_and_plot(f"IVF{nlist},PQ{M}x4fs", num_threads=8)
|
||||
eval_and_plot(f"IVF{nlist},PQ{M}x4fs", single_query=True, implem=0, num_threads=8)
|
||||
eval_and_plot(f"IVF{nlist},PQ{M}x4fs", single_query=True, implem=14, num_threads=8)
|
||||
eval_and_plot(f"IVF{nlist},PQ{M}x4fs", single_query=True, implem=15, num_threads=8)
|
||||
eval_and_plot(
|
||||
f"IVF{nlist},PQ{M}x4fs", single_query=True, implem=0, num_threads=8
|
||||
)
|
||||
eval_and_plot(
|
||||
f"IVF{nlist},PQ{M}x4fs", single_query=True, implem=14, num_threads=8
|
||||
)
|
||||
eval_and_plot(
|
||||
f"IVF{nlist},PQ{M}x4fs", single_query=True, implem=15, num_threads=8
|
||||
)
|
||||
|
||||
plt.title("Indices on Bigann50M")
|
||||
plt.xlabel("1Recall@{}".format(k))
|
||||
plt.ylabel("QPS")
|
||||
plt.legend(bbox_to_anchor=(1.02, 0.1), loc='upper left', borderaxespad=0)
|
||||
plt.savefig("bench_ivf_fastscan.png", bbox_inches='tight')
|
||||
plt.legend(bbox_to_anchor=(1.02, 0.1), loc="upper left", borderaxespad=0)
|
||||
plt.savefig("bench_ivf_fastscan.png", bbox_inches="tight")
|
||||
|
||||
@@ -17,9 +17,7 @@ except ImportError:
|
||||
from faiss.contrib.datasets import DatasetSIFT1M, DatasetGIST1M
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--dataset", default="gist1m", choices=["sift1m", "gist1m"]
|
||||
)
|
||||
parser.add_argument("--dataset", default="gist1m", choices=["sift1m", "gist1m"])
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dataset == "sift1m":
|
||||
|
||||
@@ -26,11 +26,9 @@ import argparse
|
||||
import rmm
|
||||
|
||||
try:
|
||||
from faiss.contrib.datasets_fb import \
|
||||
DatasetSIFT1M
|
||||
from faiss.contrib.datasets_fb import DatasetSIFT1M
|
||||
except ImportError:
|
||||
from faiss.contrib.datasets import \
|
||||
DatasetSIFT1M
|
||||
from faiss.contrib.datasets import DatasetSIFT1M
|
||||
|
||||
|
||||
# ds = DatasetDeep1B(10**6)
|
||||
@@ -58,25 +56,33 @@ def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
group = parser.add_argument_group('benchmarking options')
|
||||
group = parser.add_argument_group("benchmarking options")
|
||||
|
||||
aa('--bm_train', default=True,
|
||||
help='whether to benchmark train operation on GPU index')
|
||||
aa('--bm_add', default=True,
|
||||
help='whether to benchmark add operation on GPU index')
|
||||
aa('--bm_search', default=True,
|
||||
help='whether to benchmark search operation on GPU index')
|
||||
aa(
|
||||
"--bm_train",
|
||||
default=True,
|
||||
help="whether to benchmark train operation on GPU index",
|
||||
)
|
||||
aa(
|
||||
"--bm_add",
|
||||
default=True,
|
||||
help="whether to benchmark add operation on GPU index",
|
||||
)
|
||||
aa(
|
||||
"--bm_search",
|
||||
default=True,
|
||||
help="whether to benchmark search operation on GPU index",
|
||||
)
|
||||
|
||||
|
||||
group = parser.add_argument_group('IVF options')
|
||||
aa('--nlist', default=1024, type=int,
|
||||
help="number of IVF centroids")
|
||||
group = parser.add_argument_group("IVF options")
|
||||
aa("--nlist", default=1024, type=int, help="number of IVF centroids")
|
||||
|
||||
|
||||
group = parser.add_argument_group('searching')
|
||||
group = parser.add_argument_group("searching")
|
||||
|
||||
aa('--k', default=10, type=int, help='nb of nearest neighbors')
|
||||
aa('--nprobe', default=10, help='nb of IVF lists to probe')
|
||||
aa("--k", default=10, type=int, help="nb of nearest neighbors")
|
||||
aa("--nprobe", default=10, help="nb of IVF lists to probe")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -97,10 +103,10 @@ def bench_train_milliseconds(trainVecs, ncols, nlist, use_cuvs):
|
||||
index = faiss.GpuIndexIVFFlat(res, ncols, nlist, faiss.METRIC_L2, config)
|
||||
t0 = time.time()
|
||||
index.train(trainVecs)
|
||||
return 1000*(time.time() - t0)
|
||||
return 1000 * (time.time() - t0)
|
||||
|
||||
|
||||
#warmup
|
||||
# warmup
|
||||
xw = rs.rand(nt, d)
|
||||
bench_train_milliseconds(xw, d, args.nlist, True)
|
||||
|
||||
@@ -111,20 +117,26 @@ if args.bm_train:
|
||||
print("=" * 40)
|
||||
|
||||
cuvs_gpu_train_time = bench_train_milliseconds(xt, d, args.nlist, True)
|
||||
classical_gpu_train_time = bench_train_milliseconds(xt, d, args.nlist, False)
|
||||
print("Method: IVFFlat, Operation: TRAIN, dim: %d, nlist %d, numTrain: %d, classical GPU train time: %.3f milliseconds, cuVS enabled GPU train time: %.3f milliseconds" % (
|
||||
d, args.nlist, nt, classical_gpu_train_time, cuvs_gpu_train_time))
|
||||
classical_gpu_train_time = bench_train_milliseconds(
|
||||
xt, d, args.nlist, False
|
||||
)
|
||||
print(
|
||||
"Method: IVFFlat, Operation: TRAIN, dim: %d, nlist %d, numTrain: %d, classical GPU train time: %.3f milliseconds, cuVS enabled GPU train time: %.3f milliseconds"
|
||||
% (d, args.nlist, nt, classical_gpu_train_time, cuvs_gpu_train_time)
|
||||
)
|
||||
|
||||
|
||||
def bench_add_milliseconds(addVecs, q, use_cuvs):
|
||||
# construct a GPU index using the same trained coarse quantizer
|
||||
config = faiss.GpuIndexIVFFlatConfig()
|
||||
config.use_cuvs = use_cuvs
|
||||
index_gpu = faiss.GpuIndexIVFFlat(res, q, d, args.nlist, faiss.METRIC_L2, config)
|
||||
assert(index_gpu.is_trained)
|
||||
index_gpu = faiss.GpuIndexIVFFlat(
|
||||
res, q, d, args.nlist, faiss.METRIC_L2, config
|
||||
)
|
||||
assert index_gpu.is_trained
|
||||
t0 = time.time()
|
||||
index_gpu.add(addVecs)
|
||||
return 1000*(time.time() - t0)
|
||||
return 1000 * (time.time() - t0)
|
||||
|
||||
|
||||
if args.bm_add:
|
||||
@@ -136,8 +148,10 @@ if args.bm_add:
|
||||
idx_cpu.train(xt)
|
||||
cuvs_gpu_add_time = bench_add_milliseconds(xb, quantizer, True)
|
||||
classical_gpu_add_time = bench_add_milliseconds(xb, quantizer, False)
|
||||
print("Method: IVFFlat, Operation: ADD, dim: %d, nlist %d, numAdd: %d, classical GPU add time: %.3f milliseconds, cuVS enabled GPU add time: %.3f milliseconds" % (
|
||||
d, args.nlist, nb, classical_gpu_add_time, cuvs_gpu_add_time))
|
||||
print(
|
||||
"Method: IVFFlat, Operation: ADD, dim: %d, nlist %d, numAdd: %d, classical GPU add time: %.3f milliseconds, cuVS enabled GPU add time: %.3f milliseconds"
|
||||
% (d, args.nlist, nb, classical_gpu_add_time, cuvs_gpu_add_time)
|
||||
)
|
||||
|
||||
|
||||
def bench_search_milliseconds(index, queryVecs, nprobe, k, use_cuvs):
|
||||
@@ -147,21 +161,33 @@ def bench_search_milliseconds(index, queryVecs, nprobe, k, use_cuvs):
|
||||
index_gpu.nprobe = nprobe
|
||||
t0 = time.time()
|
||||
index_gpu.search(queryVecs, k)
|
||||
return 1000*(time.time() - t0)
|
||||
return 1000 * (time.time() - t0)
|
||||
|
||||
|
||||
if args.bm_search:
|
||||
print("=" * 40)
|
||||
print("GPU Search Benchmarks")
|
||||
print("=" * 40)
|
||||
idx_cpu = faiss.IndexIVFFlat(
|
||||
faiss.IndexFlatL2(d), d, args.nlist)
|
||||
idx_cpu = faiss.IndexIVFFlat(faiss.IndexFlatL2(d), d, args.nlist)
|
||||
idx_cpu.train(xt)
|
||||
idx_cpu.add(xb)
|
||||
|
||||
cuvs_gpu_search_time = bench_search_milliseconds(
|
||||
idx_cpu, xq, args.nprobe, args.k, True)
|
||||
idx_cpu, xq, args.nprobe, args.k, True
|
||||
)
|
||||
classical_gpu_search_time = bench_search_milliseconds(
|
||||
idx_cpu, xq, args.nprobe, args.k, False)
|
||||
print("Method: IVFFlat, Operation: SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, nprobe: %d, k: %d, classical GPU search time: %.3f milliseconds, cuVS enabled GPU search time: %.3f milliseconds" % (
|
||||
d, args.nlist, nb, nq, args.nprobe, args.k, classical_gpu_search_time, cuvs_gpu_search_time))
|
||||
idx_cpu, xq, args.nprobe, args.k, False
|
||||
)
|
||||
print(
|
||||
"Method: IVFFlat, Operation: SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, nprobe: %d, k: %d, classical GPU search time: %.3f milliseconds, cuVS enabled GPU search time: %.3f milliseconds"
|
||||
% (
|
||||
d,
|
||||
args.nlist,
|
||||
nb,
|
||||
nq,
|
||||
args.nprobe,
|
||||
args.k,
|
||||
classical_gpu_search_time,
|
||||
cuvs_gpu_search_time,
|
||||
)
|
||||
)
|
||||
|
||||
+56
-35
@@ -26,11 +26,9 @@ import argparse
|
||||
import rmm
|
||||
|
||||
try:
|
||||
from faiss.contrib.datasets_fb import \
|
||||
DatasetSIFT1M
|
||||
from faiss.contrib.datasets_fb import DatasetSIFT1M
|
||||
except ImportError:
|
||||
from faiss.contrib.datasets import \
|
||||
DatasetSIFT1M
|
||||
from faiss.contrib.datasets import DatasetSIFT1M
|
||||
|
||||
|
||||
# ds = DatasetDeep1B(10**6)
|
||||
@@ -60,32 +58,45 @@ def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
group = parser.add_argument_group('benchmarking options')
|
||||
group = parser.add_argument_group("benchmarking options")
|
||||
|
||||
aa('--bm_train', default=True,
|
||||
help='whether to benchmark train operation on GPU index')
|
||||
aa('--bm_add', default=True,
|
||||
help='whether to benchmark add operation on GPU index')
|
||||
aa('--bm_search', default=True,
|
||||
help='whether to benchmark search operation on GPU index')
|
||||
aa(
|
||||
"--bm_train",
|
||||
default=True,
|
||||
help="whether to benchmark train operation on GPU index",
|
||||
)
|
||||
aa(
|
||||
"--bm_add",
|
||||
default=True,
|
||||
help="whether to benchmark add operation on GPU index",
|
||||
)
|
||||
aa(
|
||||
"--bm_search",
|
||||
default=True,
|
||||
help="whether to benchmark search operation on GPU index",
|
||||
)
|
||||
|
||||
|
||||
group = parser.add_argument_group('IVF options')
|
||||
aa('--nlist', default=1024, type=np.int64,
|
||||
help="number of IVF centroids")
|
||||
aa('--bits_per_code', default=8, type=np.int64, help='bits per code. Note that < 8 is only supported when cuVS is enabled')
|
||||
group = parser.add_argument_group("IVF options")
|
||||
aa("--nlist", default=1024, type=np.int64, help="number of IVF centroids")
|
||||
aa(
|
||||
"--bits_per_code",
|
||||
default=8,
|
||||
type=np.int64,
|
||||
help="bits per code. Note that < 8 is only supported when cuVS is enabled",
|
||||
)
|
||||
|
||||
|
||||
group = parser.add_argument_group('searching')
|
||||
group = parser.add_argument_group("searching")
|
||||
|
||||
aa('--k', default=10, type=int, help='nb of nearest neighbors')
|
||||
aa('--nprobe', default=10, help='nb of IVF lists to probe')
|
||||
aa("--k", default=10, type=int, help="nb of nearest neighbors")
|
||||
aa("--nprobe", default=10, help="nb of IVF lists to probe")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("args:", args)
|
||||
|
||||
gt = gt[:, :args.k]
|
||||
gt = gt[:, : args.k]
|
||||
nlist = args.nlist
|
||||
bits_per_code = args.bits_per_code
|
||||
|
||||
@@ -114,10 +125,10 @@ def bench_train_milliseconds(trainVecs, use_cuvs):
|
||||
index = faiss.GpuIndexIVFPQ(res, d, 1024, 32, 8, faiss.METRIC_L2, config)
|
||||
t0 = time.time()
|
||||
index.train(trainVecs)
|
||||
return 1000*(time.time() - t0)
|
||||
return 1000 * (time.time() - t0)
|
||||
|
||||
|
||||
#warmup
|
||||
# warmup
|
||||
xw = rs.rand(nt, d)
|
||||
bench_train_milliseconds(xw, True)
|
||||
|
||||
@@ -129,8 +140,10 @@ if args.bm_train:
|
||||
|
||||
cuvs_gpu_train_time = bench_train_milliseconds(xt, True)
|
||||
classical_gpu_train_time = bench_train_milliseconds(xt, False)
|
||||
print("TRAIN, dim: %d, nlist %d, numTrain: %d, classical GPU train time: %.3f milliseconds, cuVS enabled GPU train time: %.3f milliseconds" % (
|
||||
d, nlist, nt, classical_gpu_train_time, cuvs_gpu_train_time))
|
||||
print(
|
||||
"TRAIN, dim: %d, nlist %d, numTrain: %d, classical GPU train time: %.3f milliseconds, cuVS enabled GPU train time: %.3f milliseconds"
|
||||
% (d, nlist, nt, classical_gpu_train_time, cuvs_gpu_train_time)
|
||||
)
|
||||
|
||||
|
||||
def bench_add_milliseconds(addVecs, index_cpu, use_cuvs):
|
||||
@@ -138,10 +151,10 @@ def bench_add_milliseconds(addVecs, index_cpu, use_cuvs):
|
||||
config = faiss.GpuClonerOptions()
|
||||
config.use_cuvs = use_cuvs
|
||||
index_gpu = faiss.index_cpu_to_gpu(res, 0, index_cpu, config)
|
||||
assert(index_gpu.is_trained)
|
||||
assert index_gpu.is_trained
|
||||
t0 = time.time()
|
||||
index_gpu.add(addVecs)
|
||||
return 1000*(time.time() - t0)
|
||||
return 1000 * (time.time() - t0)
|
||||
|
||||
|
||||
if args.bm_add:
|
||||
@@ -153,8 +166,10 @@ if args.bm_add:
|
||||
index_cpu.train(xt)
|
||||
cuvs_gpu_add_time = bench_add_milliseconds(xb, index_cpu, True)
|
||||
classical_gpu_add_time = bench_add_milliseconds(xb, index_cpu, False)
|
||||
print("ADD, dim: %d, nlist %d, numAdd: %d, classical GPU add time: %.3f milliseconds, cuVS enabled GPU add time: %.3f milliseconds" % (
|
||||
d, nlist, nb, classical_gpu_add_time, cuvs_gpu_add_time))
|
||||
print(
|
||||
"ADD, dim: %d, nlist %d, numAdd: %d, classical GPU add time: %.3f milliseconds, cuVS enabled GPU add time: %.3f milliseconds"
|
||||
% (d, nlist, nb, classical_gpu_add_time, cuvs_gpu_add_time)
|
||||
)
|
||||
|
||||
|
||||
def bench_search_milliseconds(index, queryVecs, nprobe, k, use_cuvs):
|
||||
@@ -164,10 +179,10 @@ def bench_search_milliseconds(index, queryVecs, nprobe, k, use_cuvs):
|
||||
index_gpu.nprobe = nprobe
|
||||
t0 = time.time()
|
||||
_, I = index_gpu.search(queryVecs, k)
|
||||
return I, 1000*(time.time() - t0)
|
||||
return I, 1000 * (time.time() - t0)
|
||||
|
||||
|
||||
# Search benchmarks: both indexes have identical IVF centroids and lists.
|
||||
# Search benchmarks: both indexes have identical IVF centroids and lists.
|
||||
if args.bm_search:
|
||||
print("=" * 40)
|
||||
print("GPU Search Benchmarks")
|
||||
@@ -177,10 +192,16 @@ if args.bm_search:
|
||||
index_cpu.add(xb)
|
||||
|
||||
cuvs_indices, cuvs_gpu_search_time = bench_search_milliseconds(
|
||||
index_cpu, xq, args.nprobe, args.k, True)
|
||||
classical_gpu_indices, classical_gpu_search_time = bench_search_milliseconds(
|
||||
index_cpu, xq, args.nprobe, args.k, False)
|
||||
index_cpu, xq, args.nprobe, args.k, True
|
||||
)
|
||||
classical_gpu_indices, classical_gpu_search_time = (
|
||||
bench_search_milliseconds(index_cpu, xq, args.nprobe, args.k, False)
|
||||
)
|
||||
cuvs_recall, cuvs_qps = eval_recall(cuvs_indices, cuvs_gpu_search_time)
|
||||
classical_recall, classical_qps = eval_recall(classical_gpu_indices, classical_gpu_search_time)
|
||||
print("SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, nprobe: %d, k: %d, classical GPU qps: %.3f, cuVS enabled GPU qps: %.3f" % (
|
||||
d, nlist, nb, nq, args.nprobe, args.k, classical_qps, cuvs_qps))
|
||||
classical_recall, classical_qps = eval_recall(
|
||||
classical_gpu_indices, classical_gpu_search_time
|
||||
)
|
||||
print(
|
||||
"SEARCH, dim: %d, nlist: %d, numVecs: %d, numQuery: %d, nprobe: %d, k: %d, classical GPU qps: %.3f, cuVS enabled GPU qps: %.3f"
|
||||
% (d, nlist, nb, nq, args.nprobe, args.k, classical_qps, cuvs_qps)
|
||||
)
|
||||
|
||||
+15
-14
@@ -8,15 +8,16 @@ import faiss
|
||||
import numpy as np
|
||||
|
||||
|
||||
def do_partition(n, qin, maxval=65536, seed=123, id_type='int64'):
|
||||
def do_partition(n, qin, maxval=65536, seed=123, id_type="int64"):
|
||||
print(
|
||||
f"n={n} qin={qin} maxval={maxval} id_type={id_type} ",
|
||||
end="\t", flush=True
|
||||
end="\t",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# print("seed=", seed)
|
||||
rs = np.random.RandomState(seed)
|
||||
vals = rs.randint(maxval, size=n).astype('uint16')
|
||||
vals = rs.randint(maxval, size=n).astype("uint16")
|
||||
ids = (rs.permutation(n) + 12345).astype(id_type)
|
||||
|
||||
sp = faiss.swig_ptr
|
||||
@@ -37,13 +38,13 @@ def do_partition(n, qin, maxval=65536, seed=123, id_type='int64'):
|
||||
if isinstance(qin, int):
|
||||
q = qin
|
||||
faiss.CMax_uint16_partition_fuzzy(
|
||||
tab_a.get(), sp(ids), n, q, q, None)
|
||||
tab_a.get(), sp(ids), n, q, q, None
|
||||
)
|
||||
else:
|
||||
q_min, q_max = qin
|
||||
q = np.array([-1], dtype='uint64')
|
||||
q = np.array([-1], dtype="uint64")
|
||||
faiss.CMax_uint16_partition_fuzzy(
|
||||
tab_a.get(), sp(ids), n,
|
||||
q_min, q_max, sp(q)
|
||||
tab_a.get(), sp(ids), n, q_min, q_max, sp(q)
|
||||
)
|
||||
q = q[0]
|
||||
|
||||
@@ -56,13 +57,13 @@ def do_partition(n, qin, maxval=65536, seed=123, id_type='int64'):
|
||||
|
||||
times = np.array(times[100:]) * 1000000
|
||||
|
||||
|
||||
print(
|
||||
f"times {times.mean():.3f} µs (± {times.std():.4f} µs) nerr={nerr} "
|
||||
f"bisect {stats.bissect_cycles / 1e6:.3f} Mcy "
|
||||
f"compress {stats.compress_cycles / 1e6:.3f} Mcy"
|
||||
)
|
||||
|
||||
|
||||
do_partition(200, (100, 100))
|
||||
do_partition(200, (100, 150))
|
||||
do_partition(2000, (1000, 1000))
|
||||
@@ -71,9 +72,9 @@ do_partition(20000, (10000, 10000))
|
||||
do_partition(20000, (10000, 15000))
|
||||
|
||||
|
||||
do_partition(200, (100, 100), id_type='int32')
|
||||
do_partition(200, (100, 150), id_type='int32')
|
||||
do_partition(2000, (1000, 1000), id_type='int32')
|
||||
do_partition(2000, (1000, 1500), id_type='int32')
|
||||
do_partition(20000, (10000, 10000), id_type='int32')
|
||||
do_partition(20000, (10000, 15000), id_type='int32')
|
||||
do_partition(200, (100, 100), id_type="int32")
|
||||
do_partition(200, (100, 150), id_type="int32")
|
||||
do_partition(2000, (1000, 1000), id_type="int32")
|
||||
do_partition(2000, (1000, 1500), id_type="int32")
|
||||
do_partition(20000, (10000, 10000), id_type="int32")
|
||||
do_partition(20000, (10000, 15000), id_type="int32")
|
||||
|
||||
@@ -18,14 +18,14 @@ from datasets import ivecs_read
|
||||
|
||||
|
||||
def mmap_fvecs(fname):
|
||||
x = np.memmap(fname, dtype='int32', mode='r')
|
||||
x = np.memmap(fname, dtype="int32", mode="r")
|
||||
d = x[0]
|
||||
return x.view('float32').reshape(-1, d + 1)[:, 1:]
|
||||
return x.view("float32").reshape(-1, d + 1)[:, 1:]
|
||||
|
||||
|
||||
def mmap_bvecs(fname):
|
||||
x = np.memmap(fname, dtype='uint8', mode='r')
|
||||
d = x[:4].view('int32')[0]
|
||||
x = np.memmap(fname, dtype="uint8", mode="r")
|
||||
d = x[:4].view("int32")[0]
|
||||
return x.reshape(-1, d + 4)[:, 4:]
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ def mmap_bvecs(fname):
|
||||
#################################################################
|
||||
|
||||
|
||||
dbname = sys.argv[1]
|
||||
index_key = sys.argv[2]
|
||||
dbname = sys.argv[1]
|
||||
index_key = sys.argv[2]
|
||||
parametersets = sys.argv[3:]
|
||||
|
||||
|
||||
tmpdir = '/tmp/bench_polysemous'
|
||||
tmpdir = "/tmp/bench_polysemous"
|
||||
|
||||
if not os.path.isdir(tmpdir):
|
||||
print("%s does not exist, creating it" % tmpdir)
|
||||
@@ -53,33 +53,32 @@ if not os.path.isdir(tmpdir):
|
||||
|
||||
print("Preparing dataset", dbname)
|
||||
|
||||
if dbname.startswith('SIFT'):
|
||||
if dbname.startswith("SIFT"):
|
||||
# SIFT1M to SIFT1000M
|
||||
dbsize = int(dbname[4:-1])
|
||||
xb = mmap_bvecs('bigann/bigann_base.bvecs')
|
||||
xq = mmap_bvecs('bigann/bigann_query.bvecs')
|
||||
xt = mmap_bvecs('bigann/bigann_learn.bvecs')
|
||||
xb = mmap_bvecs("bigann/bigann_base.bvecs")
|
||||
xq = mmap_bvecs("bigann/bigann_query.bvecs")
|
||||
xt = mmap_bvecs("bigann/bigann_learn.bvecs")
|
||||
|
||||
# trim xb to correct size
|
||||
xb = xb[:dbsize * 1000 * 1000]
|
||||
xb = xb[: dbsize * 1000 * 1000]
|
||||
|
||||
gt = ivecs_read('bigann/gnd/idx_%dM.ivecs' % dbsize)
|
||||
gt = ivecs_read("bigann/gnd/idx_%dM.ivecs" % dbsize)
|
||||
|
||||
elif dbname == 'Deep1B':
|
||||
xb = mmap_fvecs('deep1b/base.fvecs')
|
||||
xq = mmap_fvecs('deep1b/deep1B_queries.fvecs')
|
||||
xt = mmap_fvecs('deep1b/learn.fvecs')
|
||||
elif dbname == "Deep1B":
|
||||
xb = mmap_fvecs("deep1b/base.fvecs")
|
||||
xq = mmap_fvecs("deep1b/deep1B_queries.fvecs")
|
||||
xt = mmap_fvecs("deep1b/learn.fvecs")
|
||||
# deep1B's train is outrageously big
|
||||
xt = xt[:10 * 1000 * 1000]
|
||||
gt = ivecs_read('deep1b/deep1B_groundtruth.ivecs')
|
||||
xt = xt[: 10 * 1000 * 1000]
|
||||
gt = ivecs_read("deep1b/deep1B_groundtruth.ivecs")
|
||||
|
||||
else:
|
||||
print('unknown dataset', dbname, file=sys.stderr)
|
||||
print("unknown dataset", dbname, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
print("sizes: B %s Q %s T %s gt %s" % (
|
||||
xb.shape, xq.shape, xt.shape, gt.shape))
|
||||
print("sizes: B %s Q %s T %s gt %s" % (xb.shape, xq.shape, xt.shape, gt.shape))
|
||||
|
||||
nq, d = xq.shape
|
||||
nb, d = xb.shape
|
||||
@@ -97,19 +96,18 @@ def choose_train_size(index_key):
|
||||
n_train = 256 * 1000
|
||||
|
||||
if "IVF" in index_key:
|
||||
matches = re.findall('IVF([0-9]+)', index_key)
|
||||
matches = re.findall("IVF([0-9]+)", index_key)
|
||||
ncentroids = int(matches[0])
|
||||
n_train = max(n_train, 100 * ncentroids)
|
||||
elif "IMI" in index_key:
|
||||
matches = re.findall('IMI2x([0-9]+)', index_key)
|
||||
matches = re.findall("IMI2x([0-9]+)", index_key)
|
||||
nbit = int(matches[0])
|
||||
n_train = max(n_train, 256 * (1 << nbit))
|
||||
return n_train
|
||||
|
||||
|
||||
def get_trained_index():
|
||||
filename = "%s/%s_%s_trained.index" % (
|
||||
tmpdir, dbname, index_key)
|
||||
filename = "%s/%s_%s_trained.index" % (tmpdir, dbname, index_key)
|
||||
|
||||
if not os.path.exists(filename):
|
||||
index = faiss.index_factory(d, index_key)
|
||||
@@ -119,7 +117,7 @@ def get_trained_index():
|
||||
xtsub = xt[:n_train]
|
||||
print("Keeping %d train vectors" % xtsub.shape[0])
|
||||
# make sure the data is actually in RAM and in float
|
||||
xtsub = xtsub.astype('float32').copy()
|
||||
xtsub = xtsub.astype("float32").copy()
|
||||
index.verbose = True
|
||||
|
||||
t0 = time.time()
|
||||
@@ -138,12 +136,13 @@ def get_trained_index():
|
||||
# Adding vectors to dataset
|
||||
#################################################################
|
||||
|
||||
|
||||
def rate_limited_imap(f, l):
|
||||
'a thread pre-processes the next element'
|
||||
"a thread pre-processes the next element"
|
||||
pool = ThreadPool(1)
|
||||
res = None
|
||||
for i in l:
|
||||
res_next = pool.apply_async(f, (i, ))
|
||||
res_next = pool.apply_async(f, (i,))
|
||||
if res:
|
||||
yield res.get()
|
||||
res = res_next
|
||||
@@ -151,20 +150,18 @@ def rate_limited_imap(f, l):
|
||||
|
||||
|
||||
def matrix_slice_iterator(x, bs):
|
||||
" iterate over the lines of x in blocks of size bs"
|
||||
"iterate over the lines of x in blocks of size bs"
|
||||
nb = x.shape[0]
|
||||
block_ranges = [(i0, min(nb, i0 + bs))
|
||||
for i0 in range(0, nb, bs)]
|
||||
block_ranges = [(i0, min(nb, i0 + bs)) for i0 in range(0, nb, bs)]
|
||||
|
||||
return rate_limited_imap(
|
||||
lambda i01: x[i01[0]:i01[1]].astype('float32').copy(),
|
||||
block_ranges)
|
||||
lambda i01: x[i01[0] : i01[1]].astype("float32").copy(), block_ranges
|
||||
)
|
||||
|
||||
|
||||
def get_populated_index():
|
||||
|
||||
filename = "%s/%s_%s_populated.index" % (
|
||||
tmpdir, dbname, index_key)
|
||||
filename = "%s/%s_%s_populated.index" % (tmpdir, dbname, index_key)
|
||||
|
||||
if not os.path.exists(filename):
|
||||
index = get_trained_index()
|
||||
@@ -172,7 +169,7 @@ def get_populated_index():
|
||||
t0 = time.time()
|
||||
for xs in matrix_slice_iterator(xb, 100000):
|
||||
i1 = i0 + xs.shape[0]
|
||||
print('\radd %d:%d, %.3f s' % (i0, i1, time.time() - t0), end=' ')
|
||||
print("\radd %d:%d, %.3f s" % (i0, i1, time.time() - t0), end=" ")
|
||||
sys.stdout.flush()
|
||||
index.add(xs)
|
||||
i0 = i1
|
||||
@@ -196,23 +193,23 @@ ps = faiss.ParameterSpace()
|
||||
ps.initialize(index)
|
||||
|
||||
# make sure queries are in RAM
|
||||
xq = xq.astype('float32').copy()
|
||||
xq = xq.astype("float32").copy()
|
||||
|
||||
# a static C++ object that collects statistics about searches
|
||||
ivfpq_stats = faiss.cvar.indexIVFPQ_stats
|
||||
ivf_stats = faiss.cvar.indexIVF_stats
|
||||
|
||||
|
||||
if parametersets == ['autotune'] or parametersets == ['autotuneMT']:
|
||||
if parametersets == ["autotune"] or parametersets == ["autotuneMT"]:
|
||||
|
||||
if parametersets == ['autotune']:
|
||||
if parametersets == ["autotune"]:
|
||||
faiss.omp_set_num_threads(1)
|
||||
|
||||
# setup the Criterion object: optimize for 1-R@1
|
||||
crit = faiss.OneRecallAtRCriterion(nq, 1)
|
||||
# by default, the criterion will request only 1 NN
|
||||
crit.nnn = 100
|
||||
crit.set_groundtruth(None, gt.astype('int64'))
|
||||
crit.set_groundtruth(None, gt.astype("int64"))
|
||||
|
||||
# then we let Faiss find the optimal parameters by itself
|
||||
print("exploring operating points")
|
||||
@@ -233,10 +230,14 @@ else:
|
||||
# we do queries in a single thread
|
||||
faiss.omp_set_num_threads(1)
|
||||
|
||||
print(' ' * len(parametersets[0]), '\t', 'R@1 R@10 R@100 time %pass')
|
||||
print(
|
||||
" " * len(parametersets[0]),
|
||||
"\t",
|
||||
"R@1 R@10 R@100 time %pass",
|
||||
)
|
||||
|
||||
for param in parametersets:
|
||||
print(param, '\t', end=' ')
|
||||
print(param, "\t", end=" ")
|
||||
sys.stdout.flush()
|
||||
ps.set_index_parameters(index, param)
|
||||
t0 = time.time()
|
||||
@@ -246,6 +247,6 @@ else:
|
||||
t1 = time.time()
|
||||
for rank in 1, 10, 100:
|
||||
n_ok = (I[:, :rank] == gt[:, :1]).sum()
|
||||
print("%.4f" % (n_ok / float(nq)), end=' ')
|
||||
print("%8.3f " % ((t1 - t0) * 1000.0 / nq), end=' ')
|
||||
print("%.4f" % (n_ok / float(nq)), end=" ")
|
||||
print("%8.3f " % ((t1 - t0) * 1000.0 / nq), end=" ")
|
||||
print("%5.2f" % (ivfpq_stats.n_hamming_pass * 100.0 / ivf_stats.ndis))
|
||||
|
||||
@@ -31,13 +31,13 @@ nt = 1
|
||||
faiss.omp_set_num_threads(1)
|
||||
|
||||
|
||||
print("PQ baseline", end=' ')
|
||||
print("PQ baseline", end=" ")
|
||||
index.search_type = faiss.IndexPQ.ST_PQ
|
||||
t, r = evaluate(index, xq, gt, 1)
|
||||
print("\t %7.3f ms per query, R@1 %.4f" % (t, r[1]))
|
||||
|
||||
for ht in 64, 62, 58, 54, 50, 46, 42, 38, 34, 30:
|
||||
print("Polysemous", ht, end=' ')
|
||||
print("Polysemous", ht, end=" ")
|
||||
index.search_type = faiss.IndexPQ.ST_polysemous
|
||||
index.polysemous_ht = ht
|
||||
t, r = evaluate(index, xq, gt, 1)
|
||||
|
||||
@@ -21,7 +21,6 @@ def run_bench(d, dsub, nbit=8, metric=None):
|
||||
pq = faiss.ProductQuantizer(d, M, nbit)
|
||||
pq.train(faiss.randn((max(1000, pq.ksub * 50), d), 123))
|
||||
|
||||
|
||||
sp = faiss.swig_ptr
|
||||
|
||||
times = []
|
||||
@@ -43,16 +42,19 @@ def run_bench(d, dsub, nbit=8, metric=None):
|
||||
else:
|
||||
assert False
|
||||
t1 = time.time()
|
||||
if run >= nrun // 5: # the rest is considered warmup
|
||||
if run >= nrun // 5: # the rest is considered warmup
|
||||
times.append((t1 - t0))
|
||||
times = np.array(times) * 1000
|
||||
|
||||
print(f"nx={nx}: {np.mean(times):.3f} ms (± {np.std(times):.4f})",
|
||||
end="\t")
|
||||
print(
|
||||
f"nx={nx}: {np.mean(times):.3f} ms (± {np.std(times):.4f})",
|
||||
end="\t",
|
||||
)
|
||||
res.append(times.mean())
|
||||
print()
|
||||
return res
|
||||
|
||||
|
||||
# for have_threads in True, False:
|
||||
for have_threads in False, True:
|
||||
|
||||
|
||||
@@ -45,11 +45,13 @@ def test_bigann10m(index_file, index_parameters):
|
||||
|
||||
index_ivf, vec_transform = unwind_index_ivf(index)
|
||||
|
||||
print('params regular transp_centroids regular R@1 R@10 R@100')
|
||||
print(
|
||||
"params regular transp_centroids regular R@1 R@10 R@100"
|
||||
)
|
||||
for index_parameter in index_parameters:
|
||||
ps.set_index_parameters(index, index_parameter)
|
||||
|
||||
print(index_parameter.ljust(70), end=' ')
|
||||
print(index_parameter.ljust(70), end=" ")
|
||||
|
||||
k = 100
|
||||
|
||||
@@ -76,28 +78,96 @@ def test_bigann10m(index_file, index_parameters):
|
||||
D, I = index.search(xq, k)
|
||||
t4_1 = time.time()
|
||||
|
||||
print(" %9.5f " % (t2_1 - t2_0), end=' ')
|
||||
print(" %9.5f " % (t3_1 - t3_0), end=' ')
|
||||
print(" %9.5f " % (t4_1 - t4_0), end=' ')
|
||||
print(" %9.5f " % (t2_1 - t2_0), end=" ")
|
||||
print(" %9.5f " % (t3_1 - t3_0), end=" ")
|
||||
print(" %9.5f " % (t4_1 - t4_0), end=" ")
|
||||
|
||||
for rank in 1, 10, 100:
|
||||
n_ok = (I[:, :rank] == gt[:, :1]).sum()
|
||||
print("%.4f" % (n_ok / float(nq)), end=' ')
|
||||
print("%.4f" % (n_ok / float(nq)), end=" ")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
faiss.contrib.datasets.dataset_basedir = '/home/aguzhva/ANN_SIFT1B/'
|
||||
faiss.contrib.datasets.dataset_basedir = "/home/aguzhva/ANN_SIFT1B/"
|
||||
|
||||
# represents OPQ32_128,IVF65536_HNSW32,PQ32 index
|
||||
index_file_1 = "/home/aguzhva/ANN_SIFT1B/run_tests/bench_ivf/indexes/hnsw32/.faissindex"
|
||||
|
||||
nprobe_values = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
|
||||
quantizer_efsearch_values = [4, 8, 16, 32, 64, 128, 256, 512]
|
||||
ht_values = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 256]
|
||||
ht_values = [
|
||||
2,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12,
|
||||
14,
|
||||
16,
|
||||
18,
|
||||
20,
|
||||
22,
|
||||
24,
|
||||
26,
|
||||
28,
|
||||
30,
|
||||
32,
|
||||
34,
|
||||
36,
|
||||
38,
|
||||
40,
|
||||
42,
|
||||
44,
|
||||
46,
|
||||
48,
|
||||
50,
|
||||
52,
|
||||
54,
|
||||
56,
|
||||
58,
|
||||
60,
|
||||
62,
|
||||
64,
|
||||
66,
|
||||
68,
|
||||
70,
|
||||
72,
|
||||
74,
|
||||
76,
|
||||
78,
|
||||
80,
|
||||
82,
|
||||
84,
|
||||
86,
|
||||
88,
|
||||
90,
|
||||
92,
|
||||
94,
|
||||
96,
|
||||
98,
|
||||
100,
|
||||
102,
|
||||
104,
|
||||
106,
|
||||
108,
|
||||
110,
|
||||
112,
|
||||
114,
|
||||
116,
|
||||
118,
|
||||
120,
|
||||
122,
|
||||
124,
|
||||
126,
|
||||
128,
|
||||
256,
|
||||
]
|
||||
|
||||
# represents OPQ32_128,IVF65536(IVF256,PQHDx4fs,RFlat),PQ32 index
|
||||
index_file_2 = "/home/aguzhva/ANN_SIFT1B/run_tests/bench_ivf/indexes/pq4/.faissindex"
|
||||
index_file_2 = (
|
||||
"/home/aguzhva/ANN_SIFT1B/run_tests/bench_ivf/indexes/pq4/.faissindex"
|
||||
)
|
||||
|
||||
quantizer_k_factor_rf_values = [1, 2, 4, 8, 16, 32, 64]
|
||||
quantizer_nprobe_values = [1, 2, 4, 8, 16, 32, 64, 128]
|
||||
@@ -110,9 +180,8 @@ if __name__ == "__main__":
|
||||
ht = random.choice(ht_values)
|
||||
index_parameters_1.append(
|
||||
"nprobe={},quantizer_efSearch={},ht={}".format(
|
||||
nprobe,
|
||||
quantizer_efsearch,
|
||||
ht)
|
||||
nprobe, quantizer_efsearch, ht
|
||||
)
|
||||
)
|
||||
|
||||
test_bigann10m(index_file_1, index_parameters_1)
|
||||
@@ -126,10 +195,8 @@ if __name__ == "__main__":
|
||||
ht = random.choice(ht_values)
|
||||
index_parameters_2.append(
|
||||
"nprobe={},quantizer_k_factor_rf={},quantizer_nprobe={},ht={}".format(
|
||||
nprobe,
|
||||
quantizer_k_factor_rf,
|
||||
quantizer_nprobe,
|
||||
ht)
|
||||
nprobe, quantizer_k_factor_rf, quantizer_nprobe, ht
|
||||
)
|
||||
)
|
||||
|
||||
test_bigann10m(index_file_2, index_parameters_2)
|
||||
|
||||
+34
-19
@@ -9,11 +9,17 @@ import time
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from faiss.contrib.datasets_fb import \
|
||||
DatasetSIFT1M, DatasetDeep1B, DatasetBigANN
|
||||
from faiss.contrib.datasets_fb import (
|
||||
DatasetSIFT1M,
|
||||
DatasetDeep1B,
|
||||
DatasetBigANN,
|
||||
)
|
||||
except ImportError:
|
||||
from faiss.contrib.datasets import \
|
||||
DatasetSIFT1M, DatasetDeep1B, DatasetBigANN
|
||||
from faiss.contrib.datasets import (
|
||||
DatasetSIFT1M,
|
||||
DatasetDeep1B,
|
||||
DatasetBigANN,
|
||||
)
|
||||
|
||||
|
||||
def eval_codec(q, xq, xb, gt):
|
||||
@@ -29,7 +35,8 @@ def eval_codec(q, xq, xb, gt):
|
||||
recall = (I[:, 0] == gt[:, 0]).sum() / nq
|
||||
print(
|
||||
f"\tencode time: {t1 - t0:.3f} reconstruction error: {recons_err:.3f} "
|
||||
f"1-recall@1: {recall:.4f} recons_err_compat {err_compat:.3f}")
|
||||
f"1-recall@1: {recall:.4f} recons_err_compat {err_compat:.3f}"
|
||||
)
|
||||
|
||||
|
||||
def eval_quantizer(q, xq, xb, gt, xt, variants=None):
|
||||
@@ -39,7 +46,7 @@ def eval_quantizer(q, xq, xb, gt, xt, variants=None):
|
||||
q.train(xt)
|
||||
t1 = time.time()
|
||||
train_t = t1 - t0
|
||||
print(f'\ttraining time: {train_t:.3f} s')
|
||||
print(f"\ttraining time: {train_t:.3f} s")
|
||||
for name, val in variants:
|
||||
if name is not None:
|
||||
print(f"{name}={val}")
|
||||
@@ -92,19 +99,19 @@ nt, d = xt.shape
|
||||
|
||||
# fastest to slowest
|
||||
|
||||
if 'lsq-gpu' in todo:
|
||||
if "lsq-gpu" in todo:
|
||||
lsq = faiss.LocalSearchQuantizer(d, M, nbits)
|
||||
ngpus = faiss.get_num_gpus()
|
||||
lsq.icm_encoder_factory = faiss.GpuIcmEncoderFactory(ngpus)
|
||||
lsq.verbose = True
|
||||
eval_quantizer(lsq, xb, xt, 'lsq-gpu')
|
||||
eval_quantizer(lsq, xb, xt, "lsq-gpu")
|
||||
|
||||
if 'pq' in todo:
|
||||
if "pq" in todo:
|
||||
pq = faiss.ProductQuantizer(d, M, nbits)
|
||||
print("===== PQ")
|
||||
eval_quantizer(pq, xq, xb, gt, xt)
|
||||
|
||||
if 'opq' in todo:
|
||||
if "opq" in todo:
|
||||
d2 = ((d + M - 1) // M) * M
|
||||
print("OPQ d2=", d2)
|
||||
opq = faiss.OPQMatrix(d, M, d2)
|
||||
@@ -116,33 +123,41 @@ if 'opq' in todo:
|
||||
print("===== PQ")
|
||||
eval_quantizer(pq, xq2, xb2, gt, xt2)
|
||||
|
||||
if 'prq' in todo:
|
||||
if "prq" in todo:
|
||||
print(f"===== PRQ{nsplits}x{Msub}x{nbits}")
|
||||
prq = faiss.ProductResidualQuantizer(d, nsplits, Msub, nbits)
|
||||
variants = [("max_beam_size", i) for i in (1, 2, 4, 8, 16, 32)]
|
||||
eval_quantizer(prq, xq, xb, gt, xt, variants=variants)
|
||||
|
||||
if 'plsq' in todo:
|
||||
if "plsq" in todo:
|
||||
print(f"===== PLSQ{nsplits}x{Msub}x{nbits}")
|
||||
plsq = faiss.ProductLocalSearchQuantizer(d, nsplits, Msub, nbits)
|
||||
variants = [("encode_ils_iters", i) for i in (2, 3, 4, 8, 16)]
|
||||
eval_quantizer(plsq, xq, xb, gt, xt, variants=variants)
|
||||
|
||||
if 'rq' in todo:
|
||||
if "rq" in todo:
|
||||
print("===== RQ")
|
||||
rq = faiss.ResidualQuantizer(d, M, nbits, )
|
||||
rq = faiss.ResidualQuantizer(
|
||||
d,
|
||||
M,
|
||||
nbits,
|
||||
)
|
||||
rq.max_beam_size
|
||||
rq.max_beam_size = 30 # for compatibility with older runs
|
||||
rq.max_beam_size = 30 # for compatibility with older runs
|
||||
# rq.train_type = faiss.ResidualQuantizer.Train_default
|
||||
# rq.verbose = True
|
||||
variants = [("max_beam_size", i) for i in (1, 2, 4, 8, 16, 32)]
|
||||
eval_quantizer(rq, xq, xb, gt, xt, variants=variants)
|
||||
|
||||
if 'rq_lut' in todo:
|
||||
if "rq_lut" in todo:
|
||||
print("===== RQ")
|
||||
rq = faiss.ResidualQuantizer(d, M, nbits, )
|
||||
rq = faiss.ResidualQuantizer(
|
||||
d,
|
||||
M,
|
||||
nbits,
|
||||
)
|
||||
rq.max_beam_size
|
||||
rq.max_beam_size = 30 # for compatibility with older runs
|
||||
rq.max_beam_size = 30 # for compatibility with older runs
|
||||
rq.use_beam_LUT
|
||||
rq.use_beam_LUT = 1
|
||||
# rq.train_type = faiss.ResidualQuantizer.Train_default
|
||||
@@ -150,7 +165,7 @@ if 'rq_lut' in todo:
|
||||
variants = [("max_beam_size", i) for i in (1, 2, 4, 8, 16, 32, 64)]
|
||||
eval_quantizer(rq, xq, xb, gt, xt, variants=variants)
|
||||
|
||||
if 'lsq' in todo:
|
||||
if "lsq" in todo:
|
||||
print("===== LSQ")
|
||||
lsq = faiss.LocalSearchQuantizer(d, M, nbits)
|
||||
variants = [("encode_ils_iters", i) for i in (2, 3, 4, 8, 16)]
|
||||
|
||||
@@ -74,7 +74,9 @@ nprobe_list = [4, 16, 64, 256]
|
||||
kfactor_list = [1, 8, 64, 256, 1024]
|
||||
|
||||
print(f"Benchmark on GIST1M with base '{factory}', k={k}, nq={nq}")
|
||||
print("nprobe k_factor recall_flat qps_flat recall_pano qps_pano dims_scanned(%) speedup(x)")
|
||||
print(
|
||||
"nprobe k_factor recall_flat qps_flat recall_pano qps_pano dims_scanned(%) speedup(x)"
|
||||
)
|
||||
|
||||
faiss.omp_set_num_threads(1)
|
||||
|
||||
@@ -111,10 +113,16 @@ for nprobe in nprobe_list:
|
||||
qps_p_list.append(qps_p)
|
||||
|
||||
# Draw speedup and recall
|
||||
plt.plot([kf, kf], [qps_f, qps_p], 'k--', linewidth=1, alpha=0.7)
|
||||
plt.plot([kf, kf], [qps_f, qps_p], "k--", linewidth=1, alpha=0.7)
|
||||
mid_y = (qps_f * qps_p) ** 0.5
|
||||
plt.text(kf + 10, mid_y, f"{speedup:.2f}x\nr={recall_p:.2f}",
|
||||
ha="left", va="center", fontsize=8)
|
||||
plt.text(
|
||||
kf + 10,
|
||||
mid_y,
|
||||
f"{speedup:.2f}x\nr={recall_p:.2f}",
|
||||
ha="left",
|
||||
va="center",
|
||||
fontsize=8,
|
||||
)
|
||||
|
||||
plt.plot(kfactor_list, qps_f_list, label="RefineFlat")
|
||||
plt.plot(kfactor_list, qps_p_list, label=f"RefineFlatPanorama({nlevels})")
|
||||
@@ -123,7 +131,9 @@ plt.ylim(bottom=100)
|
||||
plt.xlim(right=kfactor_list[-1] * 1.075)
|
||||
plt.xlabel("k_factor (k_base = k * k_factor)")
|
||||
plt.ylabel("QPS")
|
||||
plt.title(f"GIST1M, base={factory}, nprobe={fixed_nprobe}, nlevels={nlevels}, k={k}")
|
||||
plt.title(
|
||||
f"GIST1M, base={factory}, nprobe={fixed_nprobe}, nlevels={nlevels}, k={k}"
|
||||
)
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
plt.savefig("bench_refine_panorama.png", bbox_inches="tight")
|
||||
|
||||
@@ -15,25 +15,27 @@ nq, d = xq.shape
|
||||
|
||||
ncent = 256
|
||||
|
||||
variants = [(name, getattr(faiss.ScalarQuantizer, name))
|
||||
for name in dir(faiss.ScalarQuantizer)
|
||||
if name.startswith('QT_') and name != 'QT_count']
|
||||
variants = [
|
||||
(name, getattr(faiss.ScalarQuantizer, name))
|
||||
for name in dir(faiss.ScalarQuantizer)
|
||||
if name.startswith("QT_") and name != "QT_count"
|
||||
]
|
||||
|
||||
quantizer = faiss.IndexFlatL2(d)
|
||||
# quantizer.add(np.zeros((1, d), dtype='float32'))
|
||||
|
||||
if False:
|
||||
for name, qtype in [('flat', 0)] + variants:
|
||||
for name, qtype in [("flat", 0)] + variants:
|
||||
|
||||
print("============== test", name)
|
||||
t0 = time.time()
|
||||
|
||||
if name == 'flat':
|
||||
index = faiss.IndexIVFFlat(quantizer, d, ncent,
|
||||
faiss.METRIC_L2)
|
||||
if name == "flat":
|
||||
index = faiss.IndexIVFFlat(quantizer, d, ncent, faiss.METRIC_L2)
|
||||
else:
|
||||
index = faiss.IndexIVFScalarQuantizer(quantizer, d, ncent,
|
||||
qtype, faiss.METRIC_L2)
|
||||
index = faiss.IndexIVFScalarQuantizer(
|
||||
quantizer, d, ncent, qtype, faiss.METRIC_L2
|
||||
)
|
||||
|
||||
index.nprobe = 16
|
||||
print("[%.3f s] train" % (time.time() - t0))
|
||||
@@ -46,7 +48,7 @@ if False:
|
||||
|
||||
for rank in 1, 10, 100:
|
||||
n_ok = (I[:, :rank] == gt[:, :1]).sum()
|
||||
print("%.4f" % (n_ok / float(nq)), end=' ')
|
||||
print("%.4f" % (n_ok / float(nq)), end=" ")
|
||||
print()
|
||||
|
||||
if True:
|
||||
@@ -54,18 +56,19 @@ if True:
|
||||
|
||||
print("============== test", name)
|
||||
|
||||
for rsname, vals in [('RS_minmax',
|
||||
[-0.4, -0.2, -0.1, -0.05, 0.0, 0.1, 0.5]),
|
||||
('RS_meanstd', [0.8, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0]),
|
||||
('RS_quantiles', [0.02, 0.05, 0.1, 0.15]),
|
||||
('RS_optim', [0.0])]:
|
||||
for rsname, vals in [
|
||||
("RS_minmax", [-0.4, -0.2, -0.1, -0.05, 0.0, 0.1, 0.5]),
|
||||
("RS_meanstd", [0.8, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0]),
|
||||
("RS_quantiles", [0.02, 0.05, 0.1, 0.15]),
|
||||
("RS_optim", [0.0]),
|
||||
]:
|
||||
for val in vals:
|
||||
print("%-15s %5g " % (rsname, val), end=' ')
|
||||
index = faiss.IndexIVFScalarQuantizer(quantizer, d, ncent,
|
||||
qtype, faiss.METRIC_L2)
|
||||
print("%-15s %5g " % (rsname, val), end=" ")
|
||||
index = faiss.IndexIVFScalarQuantizer(
|
||||
quantizer, d, ncent, qtype, faiss.METRIC_L2
|
||||
)
|
||||
index.nprobe = 16
|
||||
index.sq.rangestat = getattr(faiss.ScalarQuantizer,
|
||||
rsname)
|
||||
index.sq.rangestat = getattr(faiss.ScalarQuantizer, rsname)
|
||||
|
||||
index.sq.rangestat_arg = val
|
||||
|
||||
@@ -77,5 +80,5 @@ if True:
|
||||
|
||||
for rank in 1, 10, 100:
|
||||
n_ok = (I[:, :rank] == gt[:, :1]).sum()
|
||||
print("%.4f" % (n_ok / float(nq)), end=' ')
|
||||
print("%.4f" % (n_ok / float(nq)), end=" ")
|
||||
print(" %.3f s" % (t1 - t0))
|
||||
|
||||
@@ -87,9 +87,7 @@ def main():
|
||||
n, d = args.n, args.d
|
||||
x = gaussian_mixture(n, d, args.k, args.seed)
|
||||
print("=== bench_super_kmeans ===")
|
||||
print(
|
||||
f"n={n} d={d} k={args.k} niter={args.niter} seed={args.seed}"
|
||||
)
|
||||
print(f"n={n} d={d} k={args.k} niter={args.niter} seed={args.seed}")
|
||||
|
||||
print("\n--- faiss.Clustering ---")
|
||||
v_dt, v_obj, v_iters = run_vanilla(x, d, args.k, args.niter, args.seed)
|
||||
|
||||
+17
-19
@@ -12,12 +12,12 @@ import time
|
||||
swig_ptr = faiss.swig_ptr
|
||||
|
||||
if False:
|
||||
a = np.arange(10, 14).astype('float32')
|
||||
b = np.arange(20, 24).astype('float32')
|
||||
a = np.arange(10, 14).astype("float32")
|
||||
b = np.arange(20, 24).astype("float32")
|
||||
|
||||
faiss.fvec_inner_product (swig_ptr(a), swig_ptr(b), 4)
|
||||
faiss.fvec_inner_product(swig_ptr(a), swig_ptr(b), 4)
|
||||
|
||||
1/0
|
||||
1 / 0
|
||||
|
||||
xd = 100
|
||||
yd = 1000000
|
||||
@@ -26,22 +26,21 @@ np.random.seed(1234)
|
||||
|
||||
faiss.omp_set_num_threads(1)
|
||||
|
||||
print('xd=%d yd=%d' % (xd, yd))
|
||||
print("xd=%d yd=%d" % (xd, yd))
|
||||
|
||||
print('Running inner products test..')
|
||||
print("Running inner products test..")
|
||||
for d in 3, 4, 12, 36, 64:
|
||||
|
||||
x = faiss.rand(xd * d).reshape(xd, d)
|
||||
y = faiss.rand(yd * d).reshape(yd, d)
|
||||
|
||||
distances = np.empty((xd, yd), dtype='float32')
|
||||
distances = np.empty((xd, yd), dtype="float32")
|
||||
|
||||
t0 = time.time()
|
||||
for i in range(xd):
|
||||
faiss.fvec_inner_products_ny(swig_ptr(distances[i]),
|
||||
swig_ptr(x[i]),
|
||||
swig_ptr(y),
|
||||
d, yd)
|
||||
faiss.fvec_inner_products_ny(
|
||||
swig_ptr(distances[i]), swig_ptr(x[i]), swig_ptr(y), d, yd
|
||||
)
|
||||
t1 = time.time()
|
||||
|
||||
# sparse verification
|
||||
@@ -53,23 +52,22 @@ for d in 3, 4, 12, 36, 64:
|
||||
num += abs(distances[xi, yi] - np.dot(x[xi], y[yi]))
|
||||
denom += abs(distances[xi, yi])
|
||||
|
||||
print('d=%d t=%.3f s diff=%g' % (d, t1 - t0, num / denom))
|
||||
print("d=%d t=%.3f s diff=%g" % (d, t1 - t0, num / denom))
|
||||
|
||||
|
||||
print('Running L2sqr test..')
|
||||
print("Running L2sqr test..")
|
||||
for d in 3, 4, 12, 36, 64:
|
||||
|
||||
x = faiss.rand(xd * d).reshape(xd, d)
|
||||
y = faiss.rand(yd * d).reshape(yd, d)
|
||||
|
||||
distances = np.empty((xd, yd), dtype='float32')
|
||||
distances = np.empty((xd, yd), dtype="float32")
|
||||
|
||||
t0 = time.time()
|
||||
for i in range(xd):
|
||||
faiss.fvec_L2sqr_ny(swig_ptr(distances[i]),
|
||||
swig_ptr(x[i]),
|
||||
swig_ptr(y),
|
||||
d, yd)
|
||||
faiss.fvec_L2sqr_ny(
|
||||
swig_ptr(distances[i]), swig_ptr(x[i]), swig_ptr(y), d, yd
|
||||
)
|
||||
t1 = time.time()
|
||||
|
||||
# sparse verification
|
||||
@@ -81,4 +79,4 @@ for d in 3, 4, 12, 36, 64:
|
||||
num += abs(distances[xi, yi] - np.sum((x[xi] - y[yi]) ** 2))
|
||||
denom += abs(distances[xi, yi])
|
||||
|
||||
print('d=%d t=%.3f s diff=%g' % (d, t1 - t0, num / denom))
|
||||
print("d=%d t=%.3f s diff=%g" % (d, t1 - t0, num / denom))
|
||||
|
||||
+3
-3
@@ -10,17 +10,17 @@ import numpy as np
|
||||
|
||||
|
||||
def ivecs_read(fname):
|
||||
a = np.fromfile(fname, dtype='int32')
|
||||
a = np.fromfile(fname, dtype="int32")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:].copy()
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
def load_sift1M():
|
||||
print("Loading sift1M...", end='', file=sys.stderr)
|
||||
print("Loading sift1M...", end="", file=sys.stderr)
|
||||
xt = fvecs_read("sift1M/sift_learn.fvecs")
|
||||
xb = fvecs_read("sift1M/sift_base.fvecs")
|
||||
xq = fvecs_read("sift1M/sift_query.fvecs")
|
||||
|
||||
@@ -17,15 +17,16 @@ class CombinedIndex:
|
||||
the info on how to perform searches
|
||||
"""
|
||||
|
||||
def __init__(self, invlist_fnames, empty_index_fname,
|
||||
masked_index_fname=None):
|
||||
def __init__(
|
||||
self, invlist_fnames, empty_index_fname, masked_index_fname=None
|
||||
):
|
||||
|
||||
self.indexes = indexes = []
|
||||
ilv = faiss.InvertedListsPtrVector()
|
||||
|
||||
for fname in invlist_fnames:
|
||||
if os.path.exists(fname):
|
||||
print('reading', fname, end='\r', flush=True)
|
||||
print("reading", fname, end="\r", flush=True)
|
||||
index = faiss.read_index(fname)
|
||||
indexes.append(index)
|
||||
il = faiss.extract_index_ivf(index).invlists
|
||||
@@ -37,23 +38,24 @@ class CombinedIndex:
|
||||
self.big_il = faiss.VStackInvertedLists(ilv.size(), ilv.data())
|
||||
if masked_index_fname:
|
||||
self.big_il_base = self.big_il
|
||||
print('loading', masked_index_fname)
|
||||
print("loading", masked_index_fname)
|
||||
self.masked_index = faiss.read_index(
|
||||
masked_index_fname,
|
||||
faiss.IO_FLAG_MMAP | faiss.IO_FLAG_READ_ONLY)
|
||||
masked_index_fname, faiss.IO_FLAG_MMAP | faiss.IO_FLAG_READ_ONLY
|
||||
)
|
||||
self.big_il = faiss.MaskedInvertedLists(
|
||||
faiss.extract_index_ivf(self.masked_index).invlists,
|
||||
self.big_il_base)
|
||||
self.big_il_base,
|
||||
)
|
||||
|
||||
print('loading empty index', empty_index_fname)
|
||||
print("loading empty index", empty_index_fname)
|
||||
self.index = faiss.read_index(empty_index_fname)
|
||||
ntotal = self.big_il.compute_ntotal()
|
||||
|
||||
print('replace invlists')
|
||||
print("replace invlists")
|
||||
index_ivf = faiss.extract_index_ivf(self.index)
|
||||
index_ivf.replace_invlists(self.big_il, False)
|
||||
index_ivf.ntotal = self.index.ntotal = ntotal
|
||||
index_ivf.parallel_mode = 1 # seems reasonable to do this all the time
|
||||
index_ivf.parallel_mode = 1 # seems reasonable to do this all the time
|
||||
|
||||
quantizer = faiss.downcast_index(index_ivf.quantizer)
|
||||
quantizer.hnsw.efSearch = 1024
|
||||
@@ -82,7 +84,6 @@ class CombinedIndex:
|
||||
coarse_dis, list_nos = quantizer.search(xq, index_ivf.nprobe)
|
||||
return xq, list_nos, coarse_dis
|
||||
|
||||
|
||||
def ivf_search_preassigned(self, xq, list_nos, coarse_dis, k):
|
||||
index_ivf = faiss.extract_index_ivf(self.index)
|
||||
n, d = xq.shape
|
||||
@@ -91,15 +92,20 @@ class CombinedIndex:
|
||||
assert list_nos.shape == coarse_dis.shape
|
||||
assert n2 == n
|
||||
assert d2 == index_ivf.nprobe
|
||||
D = np.empty((n, k), dtype='float32')
|
||||
I = np.empty((n, k), dtype='int64')
|
||||
D = np.empty((n, k), dtype="float32")
|
||||
I = np.empty((n, k), dtype="int64")
|
||||
index_ivf.search_preassigned(
|
||||
n, faiss.swig_ptr(xq), k,
|
||||
faiss.swig_ptr(list_nos), faiss.swig_ptr(coarse_dis),
|
||||
faiss.swig_ptr(D), faiss.swig_ptr(I), False)
|
||||
n,
|
||||
faiss.swig_ptr(xq),
|
||||
k,
|
||||
faiss.swig_ptr(list_nos),
|
||||
faiss.swig_ptr(coarse_dis),
|
||||
faiss.swig_ptr(D),
|
||||
faiss.swig_ptr(I),
|
||||
False,
|
||||
)
|
||||
return D, I
|
||||
|
||||
|
||||
def ivf_range_search_preassigned(self, xq, list_nos, coarse_dis, radius):
|
||||
index_ivf = faiss.extract_index_ivf(self.index)
|
||||
n, d = xq.shape
|
||||
@@ -111,9 +117,13 @@ class CombinedIndex:
|
||||
res = faiss.RangeSearchResult(n)
|
||||
|
||||
index_ivf.range_search_preassigned(
|
||||
n, faiss.swig_ptr(xq), radius,
|
||||
faiss.swig_ptr(list_nos), faiss.swig_ptr(coarse_dis),
|
||||
res)
|
||||
n,
|
||||
faiss.swig_ptr(xq),
|
||||
radius,
|
||||
faiss.swig_ptr(list_nos),
|
||||
faiss.swig_ptr(coarse_dis),
|
||||
res,
|
||||
)
|
||||
|
||||
lims = faiss.rev_swig_ptr(res.lims, n + 1).copy()
|
||||
nd = int(lims[-1])
|
||||
@@ -135,7 +145,8 @@ class CombinedIndex:
|
||||
def set_prefetch_nthread(self, nt):
|
||||
for idx in self.indexes:
|
||||
il = faiss.downcast_InvertedLists(
|
||||
faiss.extract_index_ivf(idx).invlists)
|
||||
faiss.extract_index_ivf(idx).invlists
|
||||
)
|
||||
il.prefetch_nthread = nt
|
||||
|
||||
def set_omp_num_threads(self, nt):
|
||||
@@ -143,38 +154,40 @@ class CombinedIndex:
|
||||
|
||||
|
||||
class CombinedIndexDeep1B(CombinedIndex):
|
||||
""" loads a CombinedIndex with the data from the big photodna index """
|
||||
"""loads a CombinedIndex with the data from the big photodna index"""
|
||||
|
||||
def __init__(self):
|
||||
# set some paths
|
||||
workdir = "/checkpoint/matthijs/ondisk_distributed/"
|
||||
|
||||
# empty index with the proper quantizer
|
||||
indexfname = workdir + 'trained.faissindex'
|
||||
indexfname = workdir + "trained.faissindex"
|
||||
|
||||
# index that has some invlists that override the big one
|
||||
masked_index_fname = None
|
||||
invlist_fnames = [
|
||||
'%s/hslices/slice%d.faissindex' % (workdir, i)
|
||||
for i in range(50)
|
||||
"%s/hslices/slice%d.faissindex" % (workdir, i) for i in range(50)
|
||||
]
|
||||
CombinedIndex.__init__(self, invlist_fnames, indexfname, masked_index_fname)
|
||||
CombinedIndex.__init__(
|
||||
self, invlist_fnames, indexfname, masked_index_fname
|
||||
)
|
||||
|
||||
|
||||
def ivecs_read(fname):
|
||||
a = np.fromfile(fname, dtype='int32')
|
||||
a = np.fromfile(fname, dtype="int32")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:].copy()
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
|
||||
ci = CombinedIndexDeep1B()
|
||||
print('loaded index of size ', ci.index.ntotal)
|
||||
print("loaded index of size ", ci.index.ntotal)
|
||||
|
||||
deep1bdir = "/datasets01_101/simsearch/041218/deep1b/"
|
||||
|
||||
@@ -187,7 +200,7 @@ if __name__ == '__main__':
|
||||
t0 = time.time()
|
||||
D, I = ci.search(xq, 100)
|
||||
t1 = time.time()
|
||||
print('nprobe=%d 1-recall@1=%.4f t=%.2fs' % (
|
||||
nprobe, (I[:, 0] == gt[:, 0]).sum() / len(xq),
|
||||
t1 - t0
|
||||
))
|
||||
print(
|
||||
"nprobe=%d 1-recall@1=%.4f t=%.2fs"
|
||||
% (nprobe, (I[:, 0] == gt[:, 0]).sum() / len(xq), t1 - t0)
|
||||
)
|
||||
|
||||
@@ -45,8 +45,8 @@ class DatasetAssignDispatch:
|
||||
return self.d
|
||||
|
||||
def get_subset(self, indices):
|
||||
res = np.zeros((len(indices), self.d), dtype='float32')
|
||||
nos = np.searchsorted(self.cs[1:], indices, side='right')
|
||||
res = np.zeros((len(indices), self.d), dtype="float32")
|
||||
nos = np.searchsorted(self.cs[1:], indices, side="right")
|
||||
|
||||
def handle(i):
|
||||
mask = nos == i
|
||||
@@ -58,10 +58,7 @@ class DatasetAssignDispatch:
|
||||
return res
|
||||
|
||||
def assign_to(self, centroids, weights=None):
|
||||
src = self.imap(
|
||||
lambda x: x.assign_to(centroids, weights),
|
||||
self.xes
|
||||
)
|
||||
src = self.imap(lambda x: x.assign_to(centroids, weights), self.xes)
|
||||
I = []
|
||||
D = []
|
||||
sum_per_centroid = None
|
||||
@@ -76,9 +73,9 @@ class DatasetAssignDispatch:
|
||||
|
||||
|
||||
class AssignServer(rpc.Server):
|
||||
""" Assign version that can be exposed via RPC """
|
||||
"""Assign version that can be exposed via RPC"""
|
||||
|
||||
def __init__(self, s, assign, log_prefix=''):
|
||||
def __init__(self, s, assign, log_prefix=""):
|
||||
rpc.Server.__init__(self, s, log_prefix=log_prefix)
|
||||
self.assign = assign
|
||||
|
||||
@@ -86,11 +83,9 @@ class AssignServer(rpc.Server):
|
||||
return getattr(self.assign, f)
|
||||
|
||||
|
||||
|
||||
|
||||
def do_test(todo):
|
||||
|
||||
testdata = '/datasets01_101/simsearch/041218/bigann/bigann_learn.bvecs'
|
||||
testdata = "/datasets01_101/simsearch/041218/bigann/bigann_learn.bvecs"
|
||||
|
||||
if os.path.exists(testdata):
|
||||
x = bvecs_mmap(testdata)
|
||||
@@ -108,7 +103,7 @@ def do_test(todo):
|
||||
if "0" in todo:
|
||||
# reference C++ run
|
||||
km = faiss.Kmeans(x.shape[1], 1000, niter=20, verbose=True)
|
||||
km.train(xx.astype('float32'))
|
||||
km.train(xx.astype("float32"))
|
||||
|
||||
if "1" in todo:
|
||||
# using the Faiss c++ implementation
|
||||
@@ -117,21 +112,24 @@ def do_test(todo):
|
||||
|
||||
if "2" in todo:
|
||||
# use the dispatch object (on local datasets)
|
||||
data = DatasetAssignDispatch([
|
||||
DatasetAssign(xx[20000 * i : 20000 * (i + 1)])
|
||||
for i in range(5)
|
||||
], False
|
||||
data = DatasetAssignDispatch(
|
||||
[DatasetAssign(xx[20000 * i : 20000 * (i + 1)]) for i in range(5)],
|
||||
False,
|
||||
)
|
||||
kmeans(1000, data, 20)
|
||||
|
||||
if "3" in todo:
|
||||
# same, with GPU
|
||||
ngpu = faiss.get_num_gpus()
|
||||
print('using %d GPUs' % ngpu)
|
||||
data = DatasetAssignDispatch([
|
||||
DatasetAssignGPU(xx[100000 * i // ngpu: 100000 * (i + 1) // ngpu], i)
|
||||
for i in range(ngpu)
|
||||
], True
|
||||
print("using %d GPUs" % ngpu)
|
||||
data = DatasetAssignDispatch(
|
||||
[
|
||||
DatasetAssignGPU(
|
||||
xx[100000 * i // ngpu : 100000 * (i + 1) // ngpu], i
|
||||
)
|
||||
for i in range(ngpu)
|
||||
],
|
||||
True,
|
||||
)
|
||||
kmeans(1000, data, 20)
|
||||
|
||||
@@ -142,98 +140,112 @@ def main():
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('general options')
|
||||
aa('--test', default='', help='perform tests (comma-separated numbers)')
|
||||
group = parser.add_argument_group("general options")
|
||||
aa("--test", default="", help="perform tests (comma-separated numbers)")
|
||||
|
||||
aa('--k', default=0, type=int, help='nb centroids')
|
||||
aa('--seed', default=1234, type=int, help='random seed')
|
||||
aa('--niter', default=20, type=int, help='nb iterations')
|
||||
aa('--gpu', default=-2, type=int, help='GPU to use (-2:none, -1: all)')
|
||||
aa("--k", default=0, type=int, help="nb centroids")
|
||||
aa("--seed", default=1234, type=int, help="random seed")
|
||||
aa("--niter", default=20, type=int, help="nb iterations")
|
||||
aa("--gpu", default=-2, type=int, help="GPU to use (-2:none, -1: all)")
|
||||
|
||||
group = parser.add_argument_group('I/O options')
|
||||
aa('--indata', default='',
|
||||
help='data file to load (supported formats fvecs, bvecs, npy')
|
||||
aa('--i0', default=0, type=int, help='first vector to keep')
|
||||
aa('--i1', default=-1, type=int, help='last vec to keep + 1')
|
||||
aa('--out', default='', help='file to store centroids')
|
||||
aa('--store_each_iteration', default=False, action='store_true',
|
||||
help='store centroid checkpoints')
|
||||
group = parser.add_argument_group("I/O options")
|
||||
aa(
|
||||
"--indata",
|
||||
default="",
|
||||
help="data file to load (supported formats fvecs, bvecs, npy",
|
||||
)
|
||||
aa("--i0", default=0, type=int, help="first vector to keep")
|
||||
aa("--i1", default=-1, type=int, help="last vec to keep + 1")
|
||||
aa("--out", default="", help="file to store centroids")
|
||||
aa(
|
||||
"--store_each_iteration",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="store centroid checkpoints",
|
||||
)
|
||||
|
||||
group = parser.add_argument_group('server options')
|
||||
aa('--server', action='store_true', default=False, help='run server')
|
||||
aa('--port', default=12345, type=int, help='server port')
|
||||
aa('--when_ready', default=None, help='store host:port to this file when ready')
|
||||
aa('--ipv4', default=False, action='store_true', help='force ipv4')
|
||||
group = parser.add_argument_group("server options")
|
||||
aa("--server", action="store_true", default=False, help="run server")
|
||||
aa("--port", default=12345, type=int, help="server port")
|
||||
aa(
|
||||
"--when_ready",
|
||||
default=None,
|
||||
help="store host:port to this file when ready",
|
||||
)
|
||||
aa("--ipv4", default=False, action="store_true", help="force ipv4")
|
||||
|
||||
group = parser.add_argument_group('client options')
|
||||
aa('--client', action='store_true', default=False, help='run client')
|
||||
aa('--servers', default='', help='list of server:port separated by spaces')
|
||||
group = parser.add_argument_group("client options")
|
||||
aa("--client", action="store_true", default=False, help="run client")
|
||||
aa("--servers", default="", help="list of server:port separated by spaces")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.test:
|
||||
do_test(args.test.split(','))
|
||||
do_test(args.test.split(","))
|
||||
return
|
||||
|
||||
# prepare data matrix (either local or remote)
|
||||
if args.indata:
|
||||
print('loading ', args.indata)
|
||||
if args.indata.endswith('.bvecs'):
|
||||
print("loading ", args.indata)
|
||||
if args.indata.endswith(".bvecs"):
|
||||
x = bvecs_mmap(args.indata)
|
||||
elif args.indata.endswith('.fvecs'):
|
||||
elif args.indata.endswith(".fvecs"):
|
||||
x = fvecs_mmap(args.indata)
|
||||
elif args.indata.endswith('.npy'):
|
||||
x = np.load(args.indata, mmap_mode='r')
|
||||
elif args.indata.endswith(".npy"):
|
||||
x = np.load(args.indata, mmap_mode="r")
|
||||
else:
|
||||
raise AssertionError
|
||||
|
||||
if args.i1 == -1:
|
||||
args.i1 = len(x)
|
||||
x = x[args.i0:args.i1]
|
||||
x = x[args.i0 : args.i1]
|
||||
if args.gpu == -2:
|
||||
data = DatasetAssign(x)
|
||||
else:
|
||||
print('moving to GPU')
|
||||
print("moving to GPU")
|
||||
data = DatasetAssignGPU(x, args.gpu)
|
||||
|
||||
elif args.client:
|
||||
print('connecting to servers')
|
||||
print("connecting to servers")
|
||||
|
||||
def connect_client(hostport):
|
||||
host, port = hostport.split(':')
|
||||
host, port = hostport.split(":")
|
||||
port = int(port)
|
||||
print('connecting %s:%d' % (host, port))
|
||||
print("connecting %s:%d" % (host, port))
|
||||
client = rpc.Client(host, port, v6=not args.ipv4)
|
||||
print('client %s:%d ready' % (host, port))
|
||||
print("client %s:%d ready" % (host, port))
|
||||
return client
|
||||
|
||||
hostports = args.servers.strip().split(' ')
|
||||
hostports = args.servers.strip().split(" ")
|
||||
# pool = ThreadPool(len(hostports))
|
||||
|
||||
data = DatasetAssignDispatch(
|
||||
list(map(connect_client, hostports)),
|
||||
True
|
||||
)
|
||||
data = DatasetAssignDispatch(list(map(connect_client, hostports)), True)
|
||||
else:
|
||||
raise AssertionError
|
||||
|
||||
|
||||
if args.server:
|
||||
print('starting server')
|
||||
print("starting server")
|
||||
log_prefix = f"{rpc.socket.gethostname()}:{args.port}"
|
||||
rpc.run_server(
|
||||
lambda s: AssignServer(s, data, log_prefix=log_prefix),
|
||||
args.port, report_to_file=args.when_ready,
|
||||
v6=not args.ipv4)
|
||||
args.port,
|
||||
report_to_file=args.when_ready,
|
||||
v6=not args.ipv4,
|
||||
)
|
||||
|
||||
else:
|
||||
print('running kmeans')
|
||||
centroids = kmeans(args.k, data, niter=args.niter, seed=args.seed,
|
||||
checkpoint=args.out if args.store_each_iteration else None)
|
||||
if args.out != '':
|
||||
print('writing centroids to', args.out)
|
||||
print("running kmeans")
|
||||
centroids = kmeans(
|
||||
args.k,
|
||||
data,
|
||||
niter=args.niter,
|
||||
seed=args.seed,
|
||||
checkpoint=args.out if args.store_each_iteration else None,
|
||||
)
|
||||
if args.out != "":
|
||||
print("writing centroids to", args.out)
|
||||
np.save(args.out, centroids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -43,13 +43,13 @@ sindex.set_omp_num_threads(64)
|
||||
|
||||
|
||||
def ivecs_read(fname):
|
||||
a = np.fromfile(fname, dtype='int32')
|
||||
a = np.fromfile(fname, dtype="int32")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:].copy()
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
deep1bdir = "/datasets01_101/simsearch/041218/deep1b/"
|
||||
@@ -64,7 +64,7 @@ for nprobe in 1, 10, 100, 1000:
|
||||
t0 = time.time()
|
||||
D, I = sindex.search(xq, 100)
|
||||
t1 = time.time()
|
||||
print('nprobe=%d 1-recall@1=%.4f t=%.2fs' % (
|
||||
nprobe, (I[:, 0] == gt[:, 0]).sum() / len(xq),
|
||||
t1 - t0
|
||||
))
|
||||
print(
|
||||
"nprobe=%d 1-recall@1=%.4f t=%.2fs"
|
||||
% (nprobe, (I[:, 0] == gt[:, 0]).sum() / len(xq), t1 - t0)
|
||||
)
|
||||
|
||||
@@ -11,13 +11,13 @@ from multiprocessing.pool import ThreadPool
|
||||
|
||||
|
||||
def ivecs_mmap(fname):
|
||||
a = np.memmap(fname, dtype='int32', mode='r')
|
||||
a = np.memmap(fname, dtype="int32", mode="r")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:]
|
||||
|
||||
|
||||
def fvecs_mmap(fname):
|
||||
return ivecs_mmap(fname).view('float32')
|
||||
return ivecs_mmap(fname).view("float32")
|
||||
|
||||
|
||||
def produce_batches(args):
|
||||
@@ -27,8 +27,10 @@ def produce_batches(args):
|
||||
if args.i1 == -1:
|
||||
args.i1 = len(x)
|
||||
|
||||
print("Iterating on vectors %d:%d from %s by batches of size %d" % (
|
||||
args.i0, args.i1, args.input, args.bs))
|
||||
print(
|
||||
"Iterating on vectors %d:%d from %s by batches of size %d"
|
||||
% (args.i0, args.i1, args.input, args.bs)
|
||||
)
|
||||
|
||||
for j0 in range(args.i0, args.i1, args.bs):
|
||||
j1 = min(j0 + args.bs, args.i1)
|
||||
@@ -36,7 +38,7 @@ def produce_batches(args):
|
||||
|
||||
|
||||
def rate_limited_iter(l):
|
||||
'a thread pre-processes the next element'
|
||||
"a thread pre-processes the next element"
|
||||
pool = ThreadPool(1)
|
||||
res = None
|
||||
|
||||
@@ -55,42 +57,49 @@ def rate_limited_iter(l):
|
||||
yield res
|
||||
res = res_next
|
||||
|
||||
|
||||
deep1bdir = "/datasets01_101/simsearch/041218/deep1b/"
|
||||
workdir = "/checkpoint/matthijs/ondisk_distributed/"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='make index for a subset of the data')
|
||||
description="make index for a subset of the data"
|
||||
)
|
||||
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('index type')
|
||||
aa('--inputindex',
|
||||
default=workdir + 'trained.faissindex',
|
||||
help='empty input index to fill in')
|
||||
aa('--nt', default=-1, type=int, help='nb of openmp threads to use')
|
||||
group = parser.add_argument_group("index type")
|
||||
aa(
|
||||
"--inputindex",
|
||||
default=workdir + "trained.faissindex",
|
||||
help="empty input index to fill in",
|
||||
)
|
||||
aa("--nt", default=-1, type=int, help="nb of openmp threads to use")
|
||||
|
||||
group = parser.add_argument_group('db options')
|
||||
aa('--input', default=deep1bdir + "base.fvecs")
|
||||
aa('--bs', default=2**18, type=int,
|
||||
help='batch size for db access')
|
||||
aa('--i0', default=0, type=int, help='lower bound to index')
|
||||
aa('--i1', default=-1, type=int, help='upper bound of vectors to index')
|
||||
group = parser.add_argument_group("db options")
|
||||
aa("--input", default=deep1bdir + "base.fvecs")
|
||||
aa("--bs", default=2**18, type=int, help="batch size for db access")
|
||||
aa("--i0", default=0, type=int, help="lower bound to index")
|
||||
aa("--i1", default=-1, type=int, help="upper bound of vectors to index")
|
||||
|
||||
group = parser.add_argument_group('output')
|
||||
aa('-o', default='/tmp/x', help='output index')
|
||||
aa('--keepquantizer', default=False, action='store_true',
|
||||
help='by default we remove the data from the quantizer to save space')
|
||||
group = parser.add_argument_group("output")
|
||||
aa("-o", default="/tmp/x", help="output index")
|
||||
aa(
|
||||
"--keepquantizer",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="by default we remove the data from the quantizer to save space",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
print('args=', args)
|
||||
print("args=", args)
|
||||
|
||||
print('start accessing data')
|
||||
print("start accessing data")
|
||||
src = produce_batches(args)
|
||||
|
||||
print('loading index', args.inputindex)
|
||||
print("loading index", args.inputindex)
|
||||
index = faiss.read_index(args.inputindex)
|
||||
|
||||
if args.nt != -1:
|
||||
@@ -99,21 +108,24 @@ def main():
|
||||
t0 = time.time()
|
||||
ntot = 0
|
||||
for ids, x in rate_limited_iter(src):
|
||||
print('add %d:%d (%.3f s)' % (ntot, ntot + ids.size, time.time() - t0))
|
||||
index.add_with_ids(np.ascontiguousarray(x, dtype='float32'), ids)
|
||||
print("add %d:%d (%.3f s)" % (ntot, ntot + ids.size, time.time() - t0))
|
||||
index.add_with_ids(np.ascontiguousarray(x, dtype="float32"), ids)
|
||||
ntot += ids.size
|
||||
|
||||
index_ivf = faiss.extract_index_ivf(index)
|
||||
print('invlists stats: imbalance %.3f' % index_ivf.invlists.imbalance_factor())
|
||||
print(
|
||||
"invlists stats: imbalance %.3f" % index_ivf.invlists.imbalance_factor()
|
||||
)
|
||||
index_ivf.invlists.print_stats()
|
||||
|
||||
if not args.keepquantizer:
|
||||
print('resetting quantizer content')
|
||||
print("resetting quantizer content")
|
||||
index_ivf = faiss.extract_index_ivf(index)
|
||||
index_ivf.quantizer.reset()
|
||||
|
||||
print('store output', args.o)
|
||||
print("store output", args.o)
|
||||
faiss.write_index(index, args.o)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -10,45 +10,45 @@ deep1bdir = "/datasets01_101/simsearch/041218/deep1b/"
|
||||
workdir = "/checkpoint/matthijs/ondisk_distributed/"
|
||||
|
||||
|
||||
print('Load centroids')
|
||||
centroids = np.load(workdir + '1M_centroids.npy')
|
||||
print("Load centroids")
|
||||
centroids = np.load(workdir + "1M_centroids.npy")
|
||||
ncent, d = centroids.shape
|
||||
|
||||
|
||||
print('apply random rotation')
|
||||
print("apply random rotation")
|
||||
rrot = faiss.RandomRotationMatrix(d, d)
|
||||
rrot.init(1234)
|
||||
centroids = rrot.apply_py(centroids)
|
||||
|
||||
print('make HNSW index as quantizer')
|
||||
print("make HNSW index as quantizer")
|
||||
quantizer = faiss.IndexHNSWFlat(d, 32)
|
||||
quantizer.hnsw.efSearch = 1024
|
||||
quantizer.hnsw.efConstruction = 200
|
||||
quantizer.add(centroids)
|
||||
|
||||
print('build index')
|
||||
print("build index")
|
||||
index = faiss.IndexPreTransform(
|
||||
rrot,
|
||||
faiss.IndexIVFScalarQuantizer(
|
||||
quantizer, d, ncent, faiss.ScalarQuantizer.QT_6bit
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def ivecs_mmap(fname):
|
||||
a = np.memmap(fname, dtype='int32', mode='r')
|
||||
a = np.memmap(fname, dtype="int32", mode="r")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:]
|
||||
|
||||
|
||||
def fvecs_mmap(fname):
|
||||
return ivecs_mmap(fname).view('float32')
|
||||
return ivecs_mmap(fname).view("float32")
|
||||
|
||||
|
||||
print('finish training index')
|
||||
xt = fvecs_mmap(deep1bdir + 'learn.fvecs')
|
||||
xt = np.ascontiguousarray(xt[:256 * 1000], dtype='float32')
|
||||
print("finish training index")
|
||||
xt = fvecs_mmap(deep1bdir + "learn.fvecs")
|
||||
xt = np.ascontiguousarray(xt[: 256 * 1000], dtype="float32")
|
||||
index.train(xt)
|
||||
|
||||
print('write output')
|
||||
faiss.write_index(index, workdir + 'trained.faissindex')
|
||||
print("write output")
|
||||
faiss.write_index(index, workdir + "trained.faissindex")
|
||||
|
||||
@@ -7,28 +7,25 @@ import faiss
|
||||
import argparse
|
||||
from multiprocessing.pool import ThreadPool
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('--inputs', nargs='*', required=True,
|
||||
help='input indexes to merge')
|
||||
parser.add_argument('--l0', type=int, default=0)
|
||||
parser.add_argument('--l1', type=int, default=-1)
|
||||
parser.add_argument(
|
||||
"--inputs", nargs="*", required=True, help="input indexes to merge"
|
||||
)
|
||||
parser.add_argument("--l0", type=int, default=0)
|
||||
parser.add_argument("--l1", type=int, default=-1)
|
||||
|
||||
parser.add_argument('--nt', default=-1,
|
||||
help='nb threads')
|
||||
parser.add_argument("--nt", default=-1, help="nb threads")
|
||||
|
||||
parser.add_argument('--output', required=True,
|
||||
help='output index filename')
|
||||
parser.add_argument('--outputIL',
|
||||
help='output invfile filename')
|
||||
parser.add_argument("--output", required=True, help="output index filename")
|
||||
parser.add_argument("--outputIL", help="output invfile filename")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.nt != -1:
|
||||
print('set nb of threads to', args.nt)
|
||||
|
||||
print("set nb of threads to", args.nt)
|
||||
|
||||
ils = faiss.InvertedListsPtrVector()
|
||||
ils_dont_dealloc = []
|
||||
@@ -38,9 +35,11 @@ if __name__ == '__main__':
|
||||
def load_index(fname):
|
||||
print("loading", fname)
|
||||
try:
|
||||
index = faiss.read_index(fname, faiss.IO_FLAG_MMAP | faiss.IO_FLAG_READ_ONLY)
|
||||
index = faiss.read_index(
|
||||
fname, faiss.IO_FLAG_MMAP | faiss.IO_FLAG_READ_ONLY
|
||||
)
|
||||
except RuntimeError as e:
|
||||
print('could not load %s: %s' % (fname, e))
|
||||
print("could not load %s: %s" % (fname, e))
|
||||
return fname, None
|
||||
|
||||
print(" %d entries" % index.ntotal)
|
||||
@@ -57,7 +56,7 @@ if __name__ == '__main__':
|
||||
il.this.own()
|
||||
ils_dont_dealloc.append(il)
|
||||
if (args.l0, args.l1) != (0, -1):
|
||||
print('restricting to lists %d:%d' % (args.l0, args.l1))
|
||||
print("restricting to lists %d:%d" % (args.l0, args.l1))
|
||||
# il = faiss.SliceInvertedLists(il, args.l0, args.l1)
|
||||
|
||||
il.crop_invlists(args.l0, args.l1)
|
||||
@@ -70,13 +69,11 @@ if __name__ == '__main__':
|
||||
print("loaded %d invlists" % ils.size())
|
||||
|
||||
if not args.outputIL:
|
||||
args.outputIL = args.output + '_invlists'
|
||||
args.outputIL = args.output + "_invlists"
|
||||
|
||||
il0 = ils.at(0)
|
||||
|
||||
il = faiss.OnDiskInvertedLists(
|
||||
il0.nlist, il0.code_size,
|
||||
args.outputIL)
|
||||
il = faiss.OnDiskInvertedLists(il0.nlist, il0.code_size, args.outputIL)
|
||||
|
||||
print("perform merge")
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ import numpy as np
|
||||
|
||||
|
||||
class MyServer(rpc.Server):
|
||||
""" Assign version that can be exposed via RPC """
|
||||
"""Assign version that can be exposed via RPC"""
|
||||
|
||||
def __init__(self, s, index):
|
||||
rpc.Server.__init__(self, s)
|
||||
self.index = index
|
||||
@@ -36,32 +37,41 @@ def main():
|
||||
def aa(*args, **kwargs):
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
group = parser.add_argument_group('server options')
|
||||
aa('--port', default=12012, type=int, help='server port')
|
||||
aa('--when_ready_dir', default=None,
|
||||
help='store host:port to this file when ready')
|
||||
aa('--ipv4', default=False, action='store_true', help='force ipv4')
|
||||
aa('--rank', default=0, type=int,
|
||||
help='rank used as index in the client table')
|
||||
group = parser.add_argument_group("server options")
|
||||
aa("--port", default=12012, type=int, help="server port")
|
||||
aa(
|
||||
"--when_ready_dir",
|
||||
default=None,
|
||||
help="store host:port to this file when ready",
|
||||
)
|
||||
aa("--ipv4", default=False, action="store_true", help="force ipv4")
|
||||
aa(
|
||||
"--rank",
|
||||
default=0,
|
||||
type=int,
|
||||
help="rank used as index in the client table",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
when_ready = None
|
||||
if args.when_ready_dir:
|
||||
when_ready = '%s/%d' % (args.when_ready_dir, args.rank)
|
||||
when_ready = "%s/%d" % (args.when_ready_dir, args.rank)
|
||||
|
||||
print('loading index')
|
||||
print("loading index")
|
||||
|
||||
index = combined_index.CombinedIndexDeep1B()
|
||||
|
||||
print('starting server')
|
||||
print("starting server")
|
||||
rpc.run_server(
|
||||
lambda s: MyServer(s, index),
|
||||
args.port, report_to_file=when_ready,
|
||||
v6=not args.ipv4)
|
||||
args.port,
|
||||
report_to_file=when_ready,
|
||||
v6=not args.ipv4,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -71,12 +81,12 @@ if __name__ == '__main__':
|
||||
|
||||
|
||||
class ResultHeap:
|
||||
""" Combine query results from a sliced dataset (for k-nn search) """
|
||||
"""Combine query results from a sliced dataset (for k-nn search)"""
|
||||
|
||||
def __init__(self, nq, k):
|
||||
" nq: number of query vectors, k: number of results per query "
|
||||
self.I = np.zeros((nq, k), dtype='int64')
|
||||
self.D = np.zeros((nq, k), dtype='float32')
|
||||
"nq: number of query vectors, k: number of results per query"
|
||||
self.I = np.zeros((nq, k), dtype="int64")
|
||||
self.D = np.zeros((nq, k), dtype="float32")
|
||||
self.nq, self.k = nq, k
|
||||
heaps = faiss.float_maxheap_array_t()
|
||||
heaps.k = k
|
||||
@@ -91,15 +101,15 @@ class ResultHeap:
|
||||
assert I.shape == (self.nq, self.k)
|
||||
I += i0
|
||||
self.heaps.addn_with_ids(
|
||||
self.k, faiss.swig_ptr(D),
|
||||
faiss.swig_ptr(I), self.k)
|
||||
self.k, faiss.swig_ptr(D), faiss.swig_ptr(I), self.k
|
||||
)
|
||||
|
||||
def finalize(self):
|
||||
self.heaps.reorder()
|
||||
|
||||
|
||||
def distribute_weights(weights, nbin):
|
||||
""" assign a set of weights to a smaller set of bins to balance them """
|
||||
"""assign a set of weights to a smaller set of bins to balance them"""
|
||||
nw = weights.size
|
||||
o = weights.argsort()
|
||||
bins = np.zeros(nbin)
|
||||
@@ -128,40 +138,37 @@ class SplitPerListIndex:
|
||||
def set_nprobe(self, nprobe):
|
||||
self.index.set_nprobe(nprobe)
|
||||
self.pool.map(
|
||||
lambda i: self.sub_indexes[i].set_nprobe(nprobe),
|
||||
range(self.ni)
|
||||
lambda i: self.sub_indexes[i].set_nprobe(nprobe), range(self.ni)
|
||||
)
|
||||
|
||||
def set_omp_num_threads(self, nt):
|
||||
faiss.omp_set_num_threads(nt)
|
||||
self.pool.map(
|
||||
lambda idx: idx.set_omp_num_threads(nt),
|
||||
self.sub_indexes
|
||||
)
|
||||
self.pool.map(lambda idx: idx.set_omp_num_threads(nt), self.sub_indexes)
|
||||
|
||||
def set_parallel_mode(self, pm):
|
||||
self.index.set_parallel_mode(pm)
|
||||
self.pool.map(
|
||||
lambda idx: idx.set_parallel_mode(pm),
|
||||
self.sub_indexes
|
||||
)
|
||||
self.pool.map(lambda idx: idx.set_parallel_mode(pm), self.sub_indexes)
|
||||
|
||||
def set_prefetch_nthread(self, nt):
|
||||
self.index.set_prefetch_nthread(nt)
|
||||
self.pool.map(
|
||||
lambda idx: idx.set_prefetch_nthread(nt),
|
||||
self.sub_indexes
|
||||
lambda idx: idx.set_prefetch_nthread(nt), self.sub_indexes
|
||||
)
|
||||
|
||||
def balance_lists(self, list_nos):
|
||||
big_il = self.index.big_il
|
||||
weights = np.array([big_il.list_size(int(i))
|
||||
for i in list_nos.ravel()])
|
||||
weights = np.array([big_il.list_size(int(i)) for i in list_nos.ravel()])
|
||||
bins, assign = distribute_weights(weights, self.ni)
|
||||
if self.verbose:
|
||||
print('bins weight range %d:%d total %d (%.2f MiB)' % (
|
||||
bins.min(), bins.max(), bins.sum(),
|
||||
bins.sum() * (self.code_size + 8) / 2 ** 20))
|
||||
print(
|
||||
"bins weight range %d:%d total %d (%.2f MiB)"
|
||||
% (
|
||||
bins.min(),
|
||||
bins.max(),
|
||||
bins.sum(),
|
||||
bins.sum() * (self.code_size + 8) / 2**20,
|
||||
)
|
||||
)
|
||||
self.nscan = bins.sum()
|
||||
return assign.reshape(list_nos.shape)
|
||||
|
||||
@@ -175,10 +182,11 @@ class SplitPerListIndex:
|
||||
list_nos_i[assign != i] = -1
|
||||
t0 = time.time()
|
||||
Di, Ii = sub_index.ivf_search_preassigned(
|
||||
xqo, list_nos_i, coarse_dis, k)
|
||||
xqo, list_nos_i, coarse_dis, k
|
||||
)
|
||||
# print(list_nos_i, Ii)
|
||||
if self.verbose:
|
||||
print('client %d: %.3f s' % (i, time.time() - t0))
|
||||
print("client %d: %.3f s" % (i, time.time() - t0))
|
||||
return Di, Ii
|
||||
|
||||
rh = ResultHeap(x.shape[0], k)
|
||||
@@ -200,9 +208,10 @@ class SplitPerListIndex:
|
||||
list_nos_i[assign != i] = -1
|
||||
t0 = time.time()
|
||||
limi, Di, Ii = sub_index.ivf_range_search_preassigned(
|
||||
xqo, list_nos_i, coarse_dis, radius)
|
||||
xqo, list_nos_i, coarse_dis, radius
|
||||
)
|
||||
if self.verbose:
|
||||
print('slice %d: %.3f s' % (i, time.time() - t0))
|
||||
print("slice %d: %.3f s" % (i, time.time() - t0))
|
||||
return limi, Di, Ii
|
||||
|
||||
D = [[] for i in range(nq)]
|
||||
@@ -211,7 +220,7 @@ class SplitPerListIndex:
|
||||
sizes = np.zeros(nq, dtype=int)
|
||||
for lims, Di, Ii in self.pool.imap(do_query, range(self.ni)):
|
||||
for i in range(nq):
|
||||
l0, l1 = lims[i:i + 2]
|
||||
l0, l1 = lims[i : i + 2]
|
||||
D[i].append(Di[l0:l1])
|
||||
I[i].append(Ii[l0:l1])
|
||||
sizes[i] += l1 - l0
|
||||
|
||||
+10
-8
@@ -23,25 +23,25 @@ def load_mnist(fname):
|
||||
print("load", fname)
|
||||
f = open(fname)
|
||||
|
||||
header = np.fromfile(f, dtype='int8', count=4*4)
|
||||
header = header.reshape(4, 4)[:, ::-1].copy().view('int32')
|
||||
header = np.fromfile(f, dtype="int8", count=4 * 4)
|
||||
header = header.reshape(4, 4)[:, ::-1].copy().view("int32")
|
||||
print(header)
|
||||
nim, xd, yd = [int(x) for x in header[1:]]
|
||||
|
||||
data = np.fromfile(f, count=nim * xd * yd,
|
||||
dtype='uint8')
|
||||
data = np.fromfile(f, count=nim * xd * yd, dtype="uint8")
|
||||
|
||||
print(data.shape, nim, xd, yd)
|
||||
data = data.reshape(nim, xd, yd)
|
||||
return data
|
||||
|
||||
|
||||
basedir = "/path/to/mnist/data"
|
||||
|
||||
x = load_mnist(basedir + 'mnist8m/mnist8m-patterns-idx3-ubyte')
|
||||
x = load_mnist(basedir + "mnist8m/mnist8m-patterns-idx3-ubyte")
|
||||
|
||||
print("reshape")
|
||||
|
||||
x = x.reshape(x.shape[0], -1).astype('float32')
|
||||
x = x.reshape(x.shape[0], -1).astype("float32")
|
||||
|
||||
|
||||
def train_kmeans(x, k, ngpu):
|
||||
@@ -66,8 +66,9 @@ def train_kmeans(x, k, ngpu):
|
||||
if ngpu == 1:
|
||||
index = faiss.GpuIndexFlatL2(res[0], d, flat_config[0])
|
||||
else:
|
||||
indexes = [faiss.GpuIndexFlatL2(res[i], d, flat_config[i])
|
||||
for i in range(ngpu)]
|
||||
indexes = [
|
||||
faiss.GpuIndexFlatL2(res[i], d, flat_config[i]) for i in range(ngpu)
|
||||
]
|
||||
index = faiss.IndexReplicas()
|
||||
for sub_index in indexes:
|
||||
index.addIndex(sub_index)
|
||||
@@ -81,6 +82,7 @@ def train_kmeans(x, k, ngpu):
|
||||
|
||||
return centroids.reshape(k, d)
|
||||
|
||||
|
||||
print("run")
|
||||
t0 = time.time()
|
||||
train_kmeans(x, k, ngpu)
|
||||
|
||||
+93
-67
@@ -27,11 +27,7 @@ class BigBatchSearcher:
|
||||
computation (parallel or not)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
index, xq, k,
|
||||
verbose=0,
|
||||
use_float16=False):
|
||||
def __init__(self, index, xq, k, verbose=0, use_float16=False):
|
||||
|
||||
# verbosity
|
||||
self.verbose = verbose
|
||||
@@ -65,9 +61,8 @@ class BigBatchSearcher:
|
||||
|
||||
def report(self, l):
|
||||
if self.verbose == 1 or (
|
||||
self.verbose == 2 and (
|
||||
l > 1000 and time.time() < self.t_display + 1.0
|
||||
)
|
||||
self.verbose == 2
|
||||
and (l > 1000 and time.time() < self.t_display + 1.0)
|
||||
):
|
||||
return
|
||||
t = time.time() - self.t0
|
||||
@@ -79,8 +74,8 @@ class BigBatchSearcher:
|
||||
f"wait out {self.t_accu[5]:.3f} "
|
||||
f"eta {datetime.timedelta(seconds=t*self.index.nlist/(l+1)-t)} "
|
||||
f"mem {faiss.get_mem_usage_kb()}",
|
||||
end="\r" if self.verbose <= 2 else "\n",
|
||||
flush=True,
|
||||
end="\r" if self.verbose <= 2 else "\n",
|
||||
flush=True,
|
||||
)
|
||||
self.t_display = time.time()
|
||||
|
||||
@@ -88,11 +83,12 @@ class BigBatchSearcher:
|
||||
self.tic("coarse quantization")
|
||||
bs = 65536
|
||||
nq = len(self.xq)
|
||||
q_assign = np.empty((nq, self.index.nprobe), dtype='int32')
|
||||
q_assign = np.empty((nq, self.index.nprobe), dtype="int32")
|
||||
for i0 in range(0, nq, bs):
|
||||
i1 = min(nq, i0 + bs)
|
||||
q_dis_i, q_assign_i = self.index.quantizer.search(
|
||||
self.xq[i0:i1], self.index.nprobe)
|
||||
self.xq[i0:i1], self.index.nprobe
|
||||
)
|
||||
# q_dis[i0:i1] = q_dis_i
|
||||
q_assign[i0:i1] = q_assign_i
|
||||
self.toc()
|
||||
@@ -101,18 +97,19 @@ class BigBatchSearcher:
|
||||
def reorder_assign(self):
|
||||
self.tic("bucket sort")
|
||||
q_assign = self.q_assign
|
||||
q_assign += 1 # move -1 -> 0
|
||||
q_assign += 1 # move -1 -> 0
|
||||
self.bucket_lims = faiss.matrix_bucket_sort_inplace(
|
||||
self.q_assign, nbucket=self.index.nlist + 1, nt=16)
|
||||
self.q_assign, nbucket=self.index.nlist + 1, nt=16
|
||||
)
|
||||
self.query_ids = self.q_assign.ravel()
|
||||
if self.verbose > 0:
|
||||
print(' number of -1s:', self.bucket_lims[1])
|
||||
print(" number of -1s:", self.bucket_lims[1])
|
||||
self.bucket_lims = self.bucket_lims[1:] # shift back to ignore -1s
|
||||
del self.q_assign # inplace so let's forget about the old version...
|
||||
del self.q_assign # inplace so let's forget about the old version...
|
||||
self.toc()
|
||||
|
||||
def prepare_bucket(self, l):
|
||||
""" prepare the queries and database items for bucket l"""
|
||||
"""prepare the queries and database items for bucket l"""
|
||||
t0 = time.time()
|
||||
index = self.index
|
||||
# prepare queries
|
||||
@@ -131,8 +128,8 @@ class BigBatchSearcher:
|
||||
xb_l = self.decode_func(xb_l)
|
||||
|
||||
if self.use_float16:
|
||||
xb_l = xb_l.astype('float16')
|
||||
xq_l = xq_l.astype('float16')
|
||||
xb_l = xb_l.astype("float16")
|
||||
xq_l = xq_l.astype("float16")
|
||||
|
||||
t2 = time.time()
|
||||
self.t_accu[0] += t1 - t0
|
||||
@@ -163,7 +160,10 @@ class BigBatchSearcher:
|
||||
"sizes": self.sizes_in_checkpoint(),
|
||||
"completed": completed,
|
||||
"rh": (self.rh.D, self.rh.I),
|
||||
}, f, -1)
|
||||
},
|
||||
f,
|
||||
-1,
|
||||
)
|
||||
os.replace(tmpname, fname)
|
||||
|
||||
def read_checkpoint(self, fname):
|
||||
@@ -176,14 +176,15 @@ class BigBatchSearcher:
|
||||
|
||||
|
||||
class BlockComputer:
|
||||
""" computation within one bucket """
|
||||
"""computation within one bucket"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
index,
|
||||
method="knn_function",
|
||||
pairwise_distances=faiss.pairwise_distances,
|
||||
knn=faiss.knn):
|
||||
self,
|
||||
index,
|
||||
method="knn_function",
|
||||
pairwise_distances=faiss.pairwise_distances,
|
||||
knn=faiss.knn,
|
||||
):
|
||||
|
||||
self.index = index
|
||||
if index.__class__ == faiss.IndexIVFFlat:
|
||||
@@ -192,14 +193,16 @@ class BlockComputer:
|
||||
by_residual = False
|
||||
elif index.__class__ == faiss.IndexIVFPQ:
|
||||
index_help = faiss.IndexPQ(
|
||||
index.d, index.pq.M, index.pq.nbits, index.metric_type)
|
||||
index.d, index.pq.M, index.pq.nbits, index.metric_type
|
||||
)
|
||||
index_help.pq = index.pq
|
||||
decode_func = index_help.pq.decode
|
||||
index_help.is_trained = True
|
||||
by_residual = index.by_residual
|
||||
elif index.__class__ == faiss.IndexIVFScalarQuantizer:
|
||||
index_help = faiss.IndexScalarQuantizer(
|
||||
index.d, index.sq.qtype, index.metric_type)
|
||||
index.d, index.sq.qtype, index.metric_type
|
||||
)
|
||||
index_help.sq = index.sq
|
||||
decode_func = index_help.sq.decode
|
||||
index_help.is_trained = True
|
||||
@@ -232,22 +235,24 @@ class BlockComputer:
|
||||
|
||||
|
||||
def big_batch_search(
|
||||
index, xq, k,
|
||||
method="knn_function",
|
||||
pairwise_distances=faiss.pairwise_distances,
|
||||
knn=faiss.knn,
|
||||
verbose=0,
|
||||
threaded=0,
|
||||
use_float16=False,
|
||||
prefetch_threads=1,
|
||||
computation_threads=1,
|
||||
q_assign=None,
|
||||
checkpoint=None,
|
||||
checkpoint_freq=7200,
|
||||
start_list=0,
|
||||
end_list=None,
|
||||
crash_at=-1
|
||||
):
|
||||
index,
|
||||
xq,
|
||||
k,
|
||||
method="knn_function",
|
||||
pairwise_distances=faiss.pairwise_distances,
|
||||
knn=faiss.knn,
|
||||
verbose=0,
|
||||
threaded=0,
|
||||
use_float16=False,
|
||||
prefetch_threads=1,
|
||||
computation_threads=1,
|
||||
q_assign=None,
|
||||
checkpoint=None,
|
||||
checkpoint_freq=7200,
|
||||
start_list=0,
|
||||
end_list=None,
|
||||
crash_at=-1,
|
||||
):
|
||||
"""
|
||||
Search queries xq in the IVF index, with a search function that collects
|
||||
batches of query vectors per inverted list. This can be faster than the
|
||||
@@ -288,10 +293,11 @@ def big_batch_search(
|
||||
assert method in ("index", "pairwise_distances", "knn_function")
|
||||
|
||||
mem_queries = xq.nbytes
|
||||
mem_assign = len(xq) * nprobe * np.dtype('int32').itemsize
|
||||
mem_res = len(xq) * k * (
|
||||
np.dtype('int64').itemsize
|
||||
+ np.dtype('float32').itemsize
|
||||
mem_assign = len(xq) * nprobe * np.dtype("int32").itemsize
|
||||
mem_res = (
|
||||
len(xq)
|
||||
* k
|
||||
* (np.dtype("int64").itemsize + np.dtype("float32").itemsize)
|
||||
)
|
||||
mem_tot = mem_queries + mem_assign + mem_res
|
||||
if verbose > 0:
|
||||
@@ -301,16 +307,11 @@ def big_batch_search(
|
||||
)
|
||||
|
||||
bbs = BigBatchSearcher(
|
||||
index, xq, k,
|
||||
verbose=verbose,
|
||||
use_float16=use_float16
|
||||
index, xq, k, verbose=verbose, use_float16=use_float16
|
||||
)
|
||||
|
||||
comp = BlockComputer(
|
||||
index,
|
||||
method=method,
|
||||
pairwise_distances=pairwise_distances,
|
||||
knn=knn
|
||||
index, method=method, pairwise_distances=pairwise_distances, knn=knn
|
||||
)
|
||||
|
||||
bbs.decode_func = comp.decode_func
|
||||
@@ -351,8 +352,8 @@ def big_batch_search(
|
||||
# parallel version with granularity 1
|
||||
|
||||
def add_results_and_prefetch(to_add, l):
|
||||
""" perform the addition for the previous bucket and
|
||||
prefetch the next (if applicable) """
|
||||
"""perform the addition for the previous bucket and
|
||||
prefetch the next (if applicable)"""
|
||||
if to_add is not None:
|
||||
bbs.add_results_to_heap(*to_add)
|
||||
if l < index.nlist:
|
||||
@@ -365,7 +366,8 @@ def big_batch_search(
|
||||
for l in range(start_list, end_list):
|
||||
bbs.report(l)
|
||||
prefetched_bucket_a = pool.apply_async(
|
||||
add_results_and_prefetch, (to_add, l + 1))
|
||||
add_results_and_prefetch, (to_add, l + 1)
|
||||
)
|
||||
q_subset, xq_l, list_ids, xb_l = prefetched_bucket
|
||||
bbs.start_t_accu()
|
||||
D, I = comp.block_search(xq_l, xb_l, list_ids, k)
|
||||
@@ -390,11 +392,13 @@ def big_batch_search(
|
||||
):
|
||||
try:
|
||||
with ThreadPool(pool_size) as pool:
|
||||
res = [pool.apply_async(
|
||||
task,
|
||||
args=(i, output_queue, input_queue))
|
||||
res = [
|
||||
pool.apply_async(
|
||||
task, args=(i, output_queue, input_queue)
|
||||
)
|
||||
for i in range(start_task, end_task)
|
||||
if i not in completed]
|
||||
if i not in completed
|
||||
]
|
||||
for r in res:
|
||||
r.get()
|
||||
pool.close()
|
||||
@@ -431,7 +435,7 @@ def big_batch_search(
|
||||
t_wait_out = 0
|
||||
while True:
|
||||
t0 = time.time()
|
||||
logging.info(f'Compute input: task {task_id}')
|
||||
logging.info(f"Compute input: task {task_id}")
|
||||
input_value = input_queue.get()
|
||||
t_wait_in = time.time() - t0
|
||||
if input_value is None:
|
||||
@@ -439,7 +443,9 @@ def big_batch_search(
|
||||
input_queue.put(None)
|
||||
break
|
||||
centroid, q_subset, xq_l, list_ids, xb_l = input_value
|
||||
logging.info(f'Compute work: task {task_id}, centroid {centroid}')
|
||||
logging.info(
|
||||
f"Compute work: task {task_id}, centroid {centroid}"
|
||||
)
|
||||
t0 = time.time()
|
||||
if computation_threads > 1:
|
||||
D, I = comp.block_search(
|
||||
@@ -448,10 +454,21 @@ def big_batch_search(
|
||||
else:
|
||||
D, I = comp.block_search(xq_l, xb_l, list_ids, k)
|
||||
t_compute = time.time() - t0
|
||||
logging.info(f'Compute output: task {task_id}, centroid {centroid}')
|
||||
logging.info(
|
||||
f"Compute output: task {task_id}, centroid {centroid}"
|
||||
)
|
||||
t0 = time.time()
|
||||
output_queue.put(
|
||||
(centroid, t_wait_in, t_wait_out, t_compute, q_subset, D, list_ids, I)
|
||||
(
|
||||
centroid,
|
||||
t_wait_in,
|
||||
t_wait_out,
|
||||
t_compute,
|
||||
q_subset,
|
||||
D,
|
||||
list_ids,
|
||||
I,
|
||||
)
|
||||
)
|
||||
t_wait_out = time.time() - t0
|
||||
logging.info(f"Compute end: {task_id}")
|
||||
@@ -487,7 +504,16 @@ def big_batch_search(
|
||||
value = compute_to_main_queue.get()
|
||||
if not value:
|
||||
break
|
||||
centroid, t_wait_in, t_wait_out, t_compute, q_subset, D, list_ids, I = value
|
||||
(
|
||||
centroid,
|
||||
t_wait_in,
|
||||
t_wait_out,
|
||||
t_compute,
|
||||
q_subset,
|
||||
D,
|
||||
list_ids,
|
||||
I,
|
||||
) = value
|
||||
# to test checkpointing
|
||||
if centroid == crash_at:
|
||||
1 / 0
|
||||
|
||||
+14
-20
@@ -15,7 +15,7 @@ from . import rpc
|
||||
|
||||
|
||||
class SearchServer(rpc.Server):
|
||||
""" Assign version that can be exposed via RPC """
|
||||
"""Assign version that can be exposed via RPC"""
|
||||
|
||||
def __init__(self, s: int, index: faiss.Index):
|
||||
rpc.Server.__init__(self, s)
|
||||
@@ -23,7 +23,7 @@ class SearchServer(rpc.Server):
|
||||
self.index_ivf = faiss.extract_index_ivf(index)
|
||||
|
||||
def set_nprobe(self, nprobe: int) -> int:
|
||||
""" set nprobe field """
|
||||
"""set nprobe field"""
|
||||
self.index_ivf.nprobe = nprobe
|
||||
|
||||
def get_ntotal(self) -> int:
|
||||
@@ -35,23 +35,22 @@ class SearchServer(rpc.Server):
|
||||
|
||||
|
||||
def run_index_server(index: faiss.Index, port: int, v6: bool = False):
|
||||
""" serve requests for that index forever """
|
||||
rpc.run_server(
|
||||
lambda s: SearchServer(s, index),
|
||||
port, v6=v6)
|
||||
"""serve requests for that index forever"""
|
||||
rpc.run_server(lambda s: SearchServer(s, index), port, v6=v6)
|
||||
|
||||
|
||||
############################################################
|
||||
# Client implementation
|
||||
############################################################
|
||||
|
||||
|
||||
class ClientIndex:
|
||||
"""manages a set of distance sub-indexes. The sub_indexes search a
|
||||
subset of the inverted lists. Searches are merged afterwards
|
||||
"""
|
||||
|
||||
def __init__(self, machine_ports: List[Tuple[str, int]], v6: bool = False):
|
||||
""" connect to a series of (host, port) pairs """
|
||||
"""connect to a series of (host, port) pairs"""
|
||||
self.sub_indexes = []
|
||||
for machine, port in machine_ports:
|
||||
self.sub_indexes.append(rpc.Client(machine, port, v6))
|
||||
@@ -64,28 +63,23 @@ class ClientIndex:
|
||||
self.verbose = False
|
||||
|
||||
def set_nprobe(self, nprobe: int) -> None:
|
||||
self.pool.map(
|
||||
lambda idx: idx.set_nprobe(nprobe),
|
||||
self.sub_indexes
|
||||
)
|
||||
self.pool.map(lambda idx: idx.set_nprobe(nprobe), self.sub_indexes)
|
||||
|
||||
def set_omp_num_threads(self, nt: int) -> None:
|
||||
self.pool.map(
|
||||
lambda idx: idx.set_omp_num_threads(nt),
|
||||
self.sub_indexes
|
||||
)
|
||||
self.pool.map(lambda idx: idx.set_omp_num_threads(nt), self.sub_indexes)
|
||||
|
||||
def get_ntotal(self) -> None:
|
||||
return sum(self.pool.map(
|
||||
lambda idx: idx.get_ntotal(),
|
||||
self.sub_indexes
|
||||
))
|
||||
return sum(
|
||||
self.pool.map(lambda idx: idx.get_ntotal(), self.sub_indexes)
|
||||
)
|
||||
|
||||
def search(self, x, k: int):
|
||||
|
||||
rh = faiss.ResultHeap(x.shape[0], k)
|
||||
|
||||
for Di, Ii in self.pool.imap(lambda idx: idx.search(x, k), self.sub_indexes):
|
||||
for Di, Ii in self.pool.imap(
|
||||
lambda idx: idx.search(x, k), self.sub_indexes
|
||||
):
|
||||
rh.add_result(Di, Ii)
|
||||
rh.finalize()
|
||||
return rh.D, rh.I
|
||||
|
||||
+81
-46
@@ -23,7 +23,9 @@ def print_nop(*arg, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def two_level_clustering(xt, nc1, nc2, rebalance=True, clustering_niter=25, **args):
|
||||
def two_level_clustering(
|
||||
xt, nc1, nc2, rebalance=True, clustering_niter=25, **args
|
||||
):
|
||||
"""
|
||||
perform 2-level clustering on a training set xt
|
||||
nc1 and nc2 are the number of clusters at each level, the final number of
|
||||
@@ -38,13 +40,13 @@ def two_level_clustering(xt, nc1, nc2, rebalance=True, clustering_niter=25, **ar
|
||||
|
||||
log = print if verbose else print_nop
|
||||
|
||||
log(f"2-level clustering of {xt.shape} nb 1st level clusters = {nc1} total {nc2}")
|
||||
log(
|
||||
f"2-level clustering of {xt.shape} nb 1st level clusters = {nc1} total {nc2}"
|
||||
)
|
||||
log("perform coarse training")
|
||||
|
||||
km = faiss.Kmeans(
|
||||
d, nc1, niter=clustering_niter,
|
||||
max_points_per_centroid=2000,
|
||||
**args
|
||||
d, nc1, niter=clustering_niter, max_points_per_centroid=2000, **args
|
||||
)
|
||||
km.train(xt)
|
||||
|
||||
@@ -58,7 +60,9 @@ def two_level_clustering(xt, nc1, nc2, rebalance=True, clustering_niter=25, **ar
|
||||
t0 = time.time()
|
||||
_, assign1 = km.assign(xt)
|
||||
bc = np.bincount(assign1, minlength=nc1)
|
||||
log(f"done in {time.time() - t0:.2f} s. Sizes of clusters {min(bc)}-{max(bc)}")
|
||||
log(
|
||||
f"done in {time.time() - t0:.2f} s. Sizes of clusters {min(bc)}-{max(bc)}"
|
||||
)
|
||||
o = assign1.argsort()
|
||||
del km
|
||||
|
||||
@@ -79,7 +83,11 @@ def two_level_clustering(xt, nc1, nc2, rebalance=True, clustering_niter=25, **ar
|
||||
t0 = time.time()
|
||||
for c1 in range(nc1):
|
||||
nc2 = int(all_nc2[c1])
|
||||
log(f"[{time.time() - t0:.2f} s] training sub-cluster {c1}/{nc1} nc2={nc2}\r", end="", flush=True)
|
||||
log(
|
||||
f"[{time.time() - t0:.2f} s] training sub-cluster {c1}/{nc1} nc2={nc2}\r",
|
||||
end="",
|
||||
flush=True,
|
||||
)
|
||||
i1 = i0 + bc[c1]
|
||||
subset = o[i0:i1]
|
||||
assert np.all(assign1[subset] == c1)
|
||||
@@ -121,7 +129,9 @@ def train_ivf_index_with_2level(index, xt, **args):
|
||||
index.train(xt)
|
||||
|
||||
|
||||
def balanced_assignment_with_penalties(x, centroids, alpha = 0.03, num_iter = 20, maxk = 100):
|
||||
def balanced_assignment_with_penalties(
|
||||
x, centroids, alpha=0.03, num_iter=20, maxk=100
|
||||
):
|
||||
"""
|
||||
Assign vectors x to centroids with a balance constraint.
|
||||
|
||||
@@ -164,15 +174,15 @@ def balanced_assignment_with_penalties(x, centroids, alpha = 0.03, num_iter = 20
|
||||
n = len(x)
|
||||
nopt = n / nc # targed bin sizes
|
||||
|
||||
# we assign to the top-maxk clusters. The final assignment will pick among these clusters.
|
||||
# we assign to the top-maxk clusters. The final assignment will pick among these clusters.
|
||||
full_d2, full_assign = faiss.knn(x, centroids, maxk)
|
||||
|
||||
# scalar penalty for each cluster
|
||||
penalties = np.ones(nc, dtype=np.float32)
|
||||
|
||||
for it in range(num_iter):
|
||||
# compute penalized assignment
|
||||
penalties2 = penalties ** 2
|
||||
# compute penalized assignment
|
||||
penalties2 = penalties**2
|
||||
full_d2_penalized = full_d2 + penalties2[full_assign]
|
||||
a0 = full_d2_penalized.argmin(axis=1)
|
||||
assign = np.take_along_axis(full_assign, a0[:, None], axis=1).ravel()
|
||||
@@ -183,13 +193,13 @@ def balanced_assignment_with_penalties(x, centroids, alpha = 0.03, num_iter = 20
|
||||
stats = dict(
|
||||
alpha=alpha,
|
||||
imf=imbalance_factor(nc, assign),
|
||||
mse = ((x - centroids[assign]) ** 2).sum(1).mean(), # recompute MSE
|
||||
mse=((x - centroids[assign]) ** 2).sum(1).mean(), # recompute MSE
|
||||
binsize_min=int(binsizes.min()),
|
||||
binsize_max=int(binsizes.max()),
|
||||
penalty_min=penalties.min(),
|
||||
penalty_max=penalties.max(),
|
||||
)
|
||||
|
||||
|
||||
return assign, stats
|
||||
|
||||
|
||||
@@ -206,7 +216,7 @@ class DatasetAssign:
|
||||
to centroids. All other implementations offer the same interface"""
|
||||
|
||||
def __init__(self, x):
|
||||
self.x = np.ascontiguousarray(x, dtype='float32')
|
||||
self.x = np.ascontiguousarray(x, dtype="float32")
|
||||
|
||||
def count(self):
|
||||
return self.x.shape[0]
|
||||
@@ -226,7 +236,7 @@ class DatasetAssign:
|
||||
I = I.ravel()
|
||||
D = D.ravel()
|
||||
nc, d = centroids.shape
|
||||
sum_per_centroid = np.zeros((nc, d), dtype='float32')
|
||||
sum_per_centroid = np.zeros((nc, d), dtype="float32")
|
||||
if weights is None:
|
||||
np.add.at(sum_per_centroid, I, self.x)
|
||||
else:
|
||||
@@ -236,15 +246,15 @@ class DatasetAssign:
|
||||
|
||||
|
||||
class DatasetAssignGPU(DatasetAssign):
|
||||
""" GPU version of the previous """
|
||||
"""GPU version of the previous"""
|
||||
|
||||
def __init__(self, x, gpu_id, verbose=False):
|
||||
DatasetAssign.__init__(self, x)
|
||||
index = faiss.IndexFlatL2(x.shape[1])
|
||||
if gpu_id >= 0:
|
||||
self.index = faiss.index_cpu_to_gpu(
|
||||
faiss.StandardGpuResources(),
|
||||
gpu_id, index)
|
||||
faiss.StandardGpuResources(), gpu_id, index
|
||||
)
|
||||
else:
|
||||
# -1 -> assign to all GPUs
|
||||
self.index = faiss.index_cpu_to_all_gpus(index)
|
||||
@@ -256,14 +266,14 @@ class DatasetAssignGPU(DatasetAssign):
|
||||
|
||||
|
||||
def sparse_assign_to_dense(xq, xb, xq_norms=None, xb_norms=None):
|
||||
""" assignment function for xq is sparse, xb is dense
|
||||
"""assignment function for xq is sparse, xb is dense
|
||||
uses a matrix multiplication. The squared norms can be provided if
|
||||
available.
|
||||
"""
|
||||
nq = xq.shape[0]
|
||||
nb = xb.shape[0]
|
||||
if xb_norms is None:
|
||||
xb_norms = (xb ** 2).sum(1)
|
||||
xb_norms = (xb**2).sum(1)
|
||||
if xq_norms is None:
|
||||
xq_norms = np.array(xq.power(2).sum(1))
|
||||
d2 = xb_norms - 2 * xq @ xb.T
|
||||
@@ -273,7 +283,8 @@ def sparse_assign_to_dense(xq, xb, xq_norms=None, xb_norms=None):
|
||||
|
||||
|
||||
def sparse_assign_to_dense_blocks(
|
||||
xq, xb, xq_norms=None, xb_norms=None, qbs=16384, bbs=16384, nt=None):
|
||||
xq, xb, xq_norms=None, xb_norms=None, qbs=16384, bbs=16384, nt=None
|
||||
):
|
||||
"""
|
||||
decomposes the sparse_assign_to_dense function into blocks to avoid a
|
||||
possible memory blow up. Can be run in multithreaded mode, because scipy's
|
||||
@@ -286,7 +297,7 @@ def sparse_assign_to_dense_blocks(
|
||||
I = -np.ones(nq, dtype=int)
|
||||
|
||||
if xb_norms is None:
|
||||
xb_norms = (xb ** 2).sum(1)
|
||||
xb_norms = (xb**2).sum(1)
|
||||
|
||||
def handle_query_block(i):
|
||||
xq_block = xq[i : i + qbs]
|
||||
@@ -334,7 +345,8 @@ class DatasetAssignSparse(DatasetAssign):
|
||||
|
||||
def perform_search(self, centroids):
|
||||
return sparse_assign_to_dense_blocks(
|
||||
self.x, centroids, xq_norms=self.squared_norms)
|
||||
self.x, centroids, xq_norms=self.squared_norms
|
||||
)
|
||||
|
||||
def assign_to(self, centroids, weights=None):
|
||||
D, I = self.perform_search(centroids)
|
||||
@@ -343,19 +355,19 @@ class DatasetAssignSparse(DatasetAssign):
|
||||
D = D.ravel()
|
||||
n = self.x.shape[0]
|
||||
if weights is None:
|
||||
weights = np.ones(n, dtype='float32')
|
||||
weights = np.ones(n, dtype="float32")
|
||||
nc = len(centroids)
|
||||
|
||||
m = scipy.sparse.csc_matrix(
|
||||
(weights, I, np.arange(n + 1)),
|
||||
shape=(nc, n))
|
||||
(weights, I, np.arange(n + 1)), shape=(nc, n)
|
||||
)
|
||||
sum_per_centroid = np.array((m * self.x).todense())
|
||||
|
||||
return I, D, sum_per_centroid
|
||||
|
||||
|
||||
def imbalance_factor(k, assign):
|
||||
assign = np.ascontiguousarray(assign, dtype='int64')
|
||||
assign = np.ascontiguousarray(assign, dtype="int64")
|
||||
return faiss.imbalance_factor(len(assign), k, faiss.swig_ptr(assign))
|
||||
|
||||
|
||||
@@ -363,13 +375,14 @@ def check_if_torch(x):
|
||||
if x.__class__ == np.ndarray:
|
||||
return False
|
||||
import torch
|
||||
|
||||
if isinstance(x, torch.Tensor):
|
||||
return True
|
||||
raise NotImplementedError(f"Unknown tensor type {type(x)}")
|
||||
|
||||
|
||||
def reassign_centroids(hassign, centroids, rs=None):
|
||||
""" reassign centroids when some of them collapse """
|
||||
"""reassign centroids when some of them collapse"""
|
||||
if rs is None:
|
||||
rs = np.random
|
||||
k, d = centroids.shape
|
||||
@@ -383,17 +396,18 @@ def reassign_centroids(hassign, centroids, rs=None):
|
||||
|
||||
if is_torch:
|
||||
import torch
|
||||
|
||||
fac = torch.ones_like(centroids[0])
|
||||
else:
|
||||
fac = np.ones_like(centroids[0])
|
||||
fac[::2] += 1 / 1024.
|
||||
fac[1::2] -= 1 / 1024.
|
||||
fac[::2] += 1 / 1024.0
|
||||
fac[1::2] -= 1 / 1024.0
|
||||
|
||||
# this is a single pass unless there are more than k/2
|
||||
# empty centroids
|
||||
while len(empty_cents) > 0:
|
||||
# choose which centroids to split (numpy)
|
||||
probas = hassign.astype('float') - 1
|
||||
probas = hassign.astype("float") - 1
|
||||
probas[probas < 0] = 0
|
||||
probas /= probas.sum()
|
||||
nnz = (probas > 0).sum()
|
||||
@@ -416,9 +430,15 @@ def reassign_centroids(hassign, centroids, rs=None):
|
||||
return nsplit
|
||||
|
||||
|
||||
|
||||
def kmeans(k, data, niter=25, seed=1234, checkpoint=None, verbose=True,
|
||||
return_stats=False):
|
||||
def kmeans(
|
||||
k,
|
||||
data,
|
||||
niter=25,
|
||||
seed=1234,
|
||||
checkpoint=None,
|
||||
verbose=True,
|
||||
return_stats=False,
|
||||
):
|
||||
"""Pure python kmeans implementation. Follows the Faiss C++ version
|
||||
quite closely, but takes a DatasetAssign instead of a training data
|
||||
matrix. Also redo is not implemented.
|
||||
@@ -429,8 +449,13 @@ def kmeans(k, data, niter=25, seed=1234, checkpoint=None, verbose=True,
|
||||
n, d = data.count(), data.dim()
|
||||
log = print if verbose else print_nop
|
||||
|
||||
log(("Clustering %d points in %dD to %d clusters, " +
|
||||
"%d iterations seed %d") % (n, d, k, niter, seed))
|
||||
log(
|
||||
(
|
||||
"Clustering %d points in %dD to %d clusters, "
|
||||
+ "%d iterations seed %d"
|
||||
)
|
||||
% (n, d, k, niter, seed)
|
||||
)
|
||||
|
||||
rs = np.random.RandomState(seed)
|
||||
print("preproc...")
|
||||
@@ -448,10 +473,10 @@ def kmeans(k, data, niter=25, seed=1234, checkpoint=None, verbose=True,
|
||||
for i in range(niter):
|
||||
t0s = time.time()
|
||||
|
||||
log('assigning', end='\r', flush=True)
|
||||
log("assigning", end="\r", flush=True)
|
||||
assign, D, sums = data.assign_to(centroids)
|
||||
|
||||
log('compute centroids', end='\r', flush=True)
|
||||
log("compute centroids", end="\r", flush=True)
|
||||
|
||||
t_search_tot += time.time() - t0s
|
||||
|
||||
@@ -462,10 +487,11 @@ def kmeans(k, data, niter=25, seed=1234, checkpoint=None, verbose=True,
|
||||
|
||||
hassign = np.bincount(assign, minlength=k)
|
||||
|
||||
fac = hassign.reshape(-1, 1).astype('float32')
|
||||
fac = hassign.reshape(-1, 1).astype("float32")
|
||||
fac[fac == 0] = 1 # quiet warning
|
||||
if is_torch:
|
||||
import torch
|
||||
|
||||
fac = torch.from_numpy(fac).to(sums.device)
|
||||
|
||||
centroids = sums / fac
|
||||
@@ -477,21 +503,30 @@ def kmeans(k, data, niter=25, seed=1234, checkpoint=None, verbose=True,
|
||||
"time": (time.time() - t0),
|
||||
"time_search": t_search_tot,
|
||||
"imbalance_factor": imbalance_factor(k, assign),
|
||||
"nsplit": nsplit
|
||||
"nsplit": nsplit,
|
||||
}
|
||||
|
||||
log((" Iteration %d (%.2f s, search %.2f s): "
|
||||
"objective=%g imbalance=%.3f nsplit=%d") % (
|
||||
i, s["time"], s["time_search"],
|
||||
err, s["imbalance_factor"],
|
||||
nsplit)
|
||||
log(
|
||||
(
|
||||
" Iteration %d (%.2f s, search %.2f s): "
|
||||
"objective=%g imbalance=%.3f nsplit=%d"
|
||||
)
|
||||
% (
|
||||
i,
|
||||
s["time"],
|
||||
s["time_search"],
|
||||
err,
|
||||
s["imbalance_factor"],
|
||||
nsplit,
|
||||
)
|
||||
)
|
||||
iteration_stats.append(s)
|
||||
|
||||
if checkpoint is not None:
|
||||
log('storing centroids in', checkpoint)
|
||||
log("storing centroids in", checkpoint)
|
||||
if is_torch:
|
||||
import torch
|
||||
|
||||
torch.save(centroids, checkpoint)
|
||||
else:
|
||||
np.save(checkpoint, centroids)
|
||||
|
||||
+149
-82
@@ -9,31 +9,38 @@ import faiss
|
||||
import getpass
|
||||
|
||||
|
||||
from .vecs_io import fvecs_read, ivecs_read, bvecs_mmap, fvecs_mmap, bvecs_iter, bvecs_iter_chunked
|
||||
from .vecs_io import (
|
||||
fvecs_read,
|
||||
ivecs_read,
|
||||
bvecs_mmap,
|
||||
fvecs_mmap,
|
||||
bvecs_iter,
|
||||
bvecs_iter_chunked,
|
||||
)
|
||||
from .exhaustive_search import knn
|
||||
|
||||
|
||||
class Dataset:
|
||||
""" Generic abstract class for a test dataset """
|
||||
"""Generic abstract class for a test dataset"""
|
||||
|
||||
def __init__(self):
|
||||
""" the constructor should set the following fields: """
|
||||
"""the constructor should set the following fields:"""
|
||||
self.d = -1
|
||||
self.metric = 'L2' # or IP
|
||||
self.metric = "L2" # or IP
|
||||
self.nq = -1
|
||||
self.nb = -1
|
||||
self.nt = -1
|
||||
|
||||
def get_queries(self):
|
||||
""" return the queries as a (nq, d) array """
|
||||
"""return the queries as a (nq, d) array"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_train(self, maxtrain=None):
|
||||
""" return the queries as a (nt, d) array """
|
||||
"""return the queries as a (nt, d) array"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_database(self):
|
||||
""" return the queries as a (nb, d) array """
|
||||
"""return the queries as a (nb, d) array"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def database_iterator(self, bs=128, split=(1, 0)):
|
||||
@@ -48,26 +55,28 @@ class Dataset:
|
||||
nsplit, rank = split
|
||||
i0, i1 = self.nb * rank // nsplit, self.nb * (rank + 1) // nsplit
|
||||
for j0 in range(i0, i1, bs):
|
||||
yield xb[j0: min(j0 + bs, i1)]
|
||||
yield xb[j0 : min(j0 + bs, i1)]
|
||||
|
||||
def get_groundtruth(self, k=None):
|
||||
""" return the ground truth for k-nearest neighbor search """
|
||||
"""return the ground truth for k-nearest neighbor search"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_groundtruth_range(self, thresh=None):
|
||||
""" return the ground truth for range search """
|
||||
"""return the ground truth for range search"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
return (f"dataset in dimension {self.d}, with metric {self.metric}, "
|
||||
f"size: Q {self.nq} B {self.nb} T {self.nt}")
|
||||
return (
|
||||
f"dataset in dimension {self.d}, with metric {self.metric}, "
|
||||
f"size: Q {self.nq} B {self.nb} T {self.nt}"
|
||||
)
|
||||
|
||||
def check_sizes(self):
|
||||
""" runs the previous and checks the sizes of the matrices """
|
||||
"""runs the previous and checks the sizes of the matrices"""
|
||||
assert self.get_queries().shape == (self.nq, self.d)
|
||||
if self.nt > 0:
|
||||
xt = self.get_train(maxtrain=123)
|
||||
assert xt.shape == (123, self.d), "shape=%s" % (xt.shape, )
|
||||
assert xt.shape == (123, self.d), "shape=%s" % (xt.shape,)
|
||||
assert self.get_database().shape == (self.nb, self.d)
|
||||
assert self.get_groundtruth(k=13).shape == (self.nq, 13)
|
||||
|
||||
@@ -77,10 +86,10 @@ class SyntheticDataset(Dataset):
|
||||
index
|
||||
"""
|
||||
|
||||
def __init__(self, d, nt, nb, nq, metric='L2', seed=1338):
|
||||
def __init__(self, d, nt, nb, nq, metric="L2", seed=1338):
|
||||
Dataset.__init__(self)
|
||||
self.d, self.nt, self.nb, self.nq = d, nt, nb, nq
|
||||
d1 = 10 # intrinsic dimension (more or less)
|
||||
d1 = 10 # intrinsic dimension (more or less)
|
||||
n = nb + nt + nq
|
||||
rs = np.random.RandomState(seed)
|
||||
x = rs.normal(size=(n, d1))
|
||||
@@ -89,11 +98,11 @@ class SyntheticDataset(Dataset):
|
||||
# higher factor (>4) -> higher frequency -> less linear
|
||||
x = x * (rs.rand(d) * 4 + 0.1)
|
||||
x = np.sin(x)
|
||||
x = x.astype('float32')
|
||||
x = x.astype("float32")
|
||||
self.metric = metric
|
||||
self.xt = x[:nt]
|
||||
self.xb = x[nt:nt + nb]
|
||||
self.xq = x[nt + nb:]
|
||||
self.xb = x[nt : nt + nb]
|
||||
self.xq = x[nt + nb :]
|
||||
|
||||
def get_queries(self):
|
||||
return self.xq
|
||||
@@ -107,8 +116,14 @@ class SyntheticDataset(Dataset):
|
||||
|
||||
def get_groundtruth(self, k=100):
|
||||
return knn(
|
||||
self.xq, self.xb, k,
|
||||
faiss.METRIC_L2 if self.metric == 'L2' else faiss.METRIC_INNER_PRODUCT
|
||||
self.xq,
|
||||
self.xb,
|
||||
k,
|
||||
(
|
||||
faiss.METRIC_L2
|
||||
if self.metric == "L2"
|
||||
else faiss.METRIC_INNER_PRODUCT
|
||||
),
|
||||
)[1]
|
||||
|
||||
|
||||
@@ -121,14 +136,15 @@ class SyntheticDataset(Dataset):
|
||||
username = getpass.getuser()
|
||||
|
||||
for dataset_basedir in (
|
||||
'/datasets01/simsearch/041218/',
|
||||
'/mnt/vol/gfsai-flash3-east/ai-group/datasets/simsearch/',
|
||||
f'/home/{username}/simsearch/data/'):
|
||||
"/datasets01/simsearch/041218/",
|
||||
"/mnt/vol/gfsai-flash3-east/ai-group/datasets/simsearch/",
|
||||
f"/home/{username}/simsearch/data/",
|
||||
):
|
||||
if os.path.exists(dataset_basedir):
|
||||
break
|
||||
else:
|
||||
# users can link their data directory to `./data`
|
||||
dataset_basedir = 'data/'
|
||||
dataset_basedir = "data/"
|
||||
|
||||
|
||||
def set_dataset_basedir(path):
|
||||
@@ -145,7 +161,7 @@ class DatasetSIFT1M(Dataset):
|
||||
def __init__(self):
|
||||
Dataset.__init__(self)
|
||||
self.d, self.nt, self.nb, self.nq = 128, 100000, 1000000, 10000
|
||||
self.basedir = dataset_basedir + 'sift1M/'
|
||||
self.basedir = dataset_basedir + "sift1M/"
|
||||
|
||||
def get_queries(self):
|
||||
return fvecs_read(self.basedir + "sift_query.fvecs")
|
||||
@@ -166,7 +182,7 @@ class DatasetSIFT1M(Dataset):
|
||||
|
||||
|
||||
def sanitize(x):
|
||||
return np.ascontiguousarray(x, dtype='float32')
|
||||
return np.ascontiguousarray(x, dtype="float32")
|
||||
|
||||
|
||||
class DatasetBigANN(Dataset):
|
||||
@@ -181,17 +197,19 @@ class DatasetBigANN(Dataset):
|
||||
self.nb_M = nb_M
|
||||
nb = nb_M * 10**6
|
||||
self.d, self.nt, self.nb, self.nq = 128, 10**8, nb, 10000
|
||||
self.basedir = dataset_basedir + 'bigann/'
|
||||
self.basedir = dataset_basedir + "bigann/"
|
||||
|
||||
def get_queries(self):
|
||||
return sanitize(bvecs_mmap(self.basedir + 'bigann_query.bvecs')[:])
|
||||
return sanitize(bvecs_mmap(self.basedir + "bigann_query.bvecs")[:])
|
||||
|
||||
def get_train(self, maxtrain=None):
|
||||
maxtrain = maxtrain if maxtrain is not None else self.nt
|
||||
return sanitize(bvecs_mmap(self.basedir + 'bigann_learn.bvecs')[:maxtrain])
|
||||
return sanitize(
|
||||
bvecs_mmap(self.basedir + "bigann_learn.bvecs")[:maxtrain]
|
||||
)
|
||||
|
||||
def get_groundtruth(self, k=None):
|
||||
gt = ivecs_read(self.basedir + 'gnd/idx_%dM.ivecs' % self.nb_M)
|
||||
gt = ivecs_read(self.basedir + "gnd/idx_%dM.ivecs" % self.nb_M)
|
||||
if k is not None:
|
||||
assert k <= 100
|
||||
gt = gt[:, :k]
|
||||
@@ -199,14 +217,16 @@ class DatasetBigANN(Dataset):
|
||||
|
||||
def get_database(self):
|
||||
assert self.nb_M < 100, "dataset too large, use iterator"
|
||||
return sanitize(bvecs_mmap(self.basedir + 'bigann_base.bvecs')[:self.nb])
|
||||
return sanitize(
|
||||
bvecs_mmap(self.basedir + "bigann_base.bvecs")[: self.nb]
|
||||
)
|
||||
|
||||
def database_iterator(self, bs=128, split=(1, 0)):
|
||||
xb = bvecs_mmap(self.basedir + 'bigann_base.bvecs')
|
||||
xb = bvecs_mmap(self.basedir + "bigann_base.bvecs")
|
||||
nsplit, rank = split
|
||||
i0, i1 = self.nb * rank // nsplit, self.nb * (rank + 1) // nsplit
|
||||
for j0 in range(i0, i1, bs):
|
||||
yield sanitize(xb[j0: min(j0 + bs, i1)])
|
||||
yield sanitize(xb[j0 : min(j0 + bs, i1)])
|
||||
|
||||
|
||||
class DatasetDeep1B(Dataset):
|
||||
@@ -219,17 +239,19 @@ class DatasetDeep1B(Dataset):
|
||||
def __init__(self, nb=10**9):
|
||||
Dataset.__init__(self)
|
||||
nb_to_name = {
|
||||
10**5: '100k',
|
||||
10**6: '1M',
|
||||
10**7: '10M',
|
||||
10**8: '100M',
|
||||
10**9: '1B'
|
||||
10**5: "100k",
|
||||
10**6: "1M",
|
||||
10**7: "10M",
|
||||
10**8: "100M",
|
||||
10**9: "1B",
|
||||
}
|
||||
assert nb in nb_to_name
|
||||
self.d, self.nt, self.nb, self.nq = 96, 358480000, nb, 10000
|
||||
self.basedir = dataset_basedir + 'deep1b/'
|
||||
self.basedir = dataset_basedir + "deep1b/"
|
||||
self.gt_fname = "%sdeep%s_groundtruth.ivecs" % (
|
||||
self.basedir, nb_to_name[self.nb])
|
||||
self.basedir,
|
||||
nb_to_name[self.nb],
|
||||
)
|
||||
|
||||
def get_queries(self):
|
||||
return sanitize(fvecs_read(self.basedir + "deep1B_queries.fvecs"))
|
||||
@@ -247,14 +269,14 @@ class DatasetDeep1B(Dataset):
|
||||
|
||||
def get_database(self):
|
||||
assert self.nb <= 10**8, "dataset too large, use iterator"
|
||||
return sanitize(fvecs_mmap(self.basedir + "base.fvecs")[:self.nb])
|
||||
return sanitize(fvecs_mmap(self.basedir + "base.fvecs")[: self.nb])
|
||||
|
||||
def database_iterator(self, bs=128, split=(1, 0)):
|
||||
xb = fvecs_mmap(self.basedir + "base.fvecs")
|
||||
nsplit, rank = split
|
||||
i0, i1 = self.nb * rank // nsplit, self.nb * (rank + 1) // nsplit
|
||||
for j0 in range(i0, i1, bs):
|
||||
yield sanitize(xb[j0: min(j0 + bs, i1)])
|
||||
yield sanitize(xb[j0 : min(j0 + bs, i1)])
|
||||
|
||||
|
||||
class DatasetGlove(Dataset):
|
||||
@@ -264,28 +286,29 @@ class DatasetGlove(Dataset):
|
||||
|
||||
def __init__(self, loc=None, download=False):
|
||||
import h5py
|
||||
|
||||
assert not download, "not implemented"
|
||||
if not loc:
|
||||
loc = dataset_basedir + 'glove/glove-100-angular.hdf5'
|
||||
self.glove_h5py = h5py.File(loc, 'r')
|
||||
loc = dataset_basedir + "glove/glove-100-angular.hdf5"
|
||||
self.glove_h5py = h5py.File(loc, "r")
|
||||
# IP and L2 are equivalent in this case, but it is traditionally seen as an IP dataset
|
||||
self.metric = 'IP'
|
||||
self.metric = "IP"
|
||||
self.d, self.nt = 100, 0
|
||||
self.nb = self.glove_h5py['train'].shape[0]
|
||||
self.nq = self.glove_h5py['test'].shape[0]
|
||||
self.nb = self.glove_h5py["train"].shape[0]
|
||||
self.nq = self.glove_h5py["test"].shape[0]
|
||||
|
||||
def get_queries(self):
|
||||
xq = np.array(self.glove_h5py['test'])
|
||||
xq = np.array(self.glove_h5py["test"])
|
||||
faiss.normalize_L2(xq)
|
||||
return xq
|
||||
|
||||
def get_database(self):
|
||||
xb = np.array(self.glove_h5py['train'])
|
||||
xb = np.array(self.glove_h5py["train"])
|
||||
faiss.normalize_L2(xb)
|
||||
return xb
|
||||
|
||||
def get_groundtruth(self, k=None):
|
||||
gt = self.glove_h5py['neighbors']
|
||||
gt = self.glove_h5py["neighbors"]
|
||||
if k is not None:
|
||||
assert k <= 100
|
||||
gt = gt[:, :k]
|
||||
@@ -301,21 +324,23 @@ class DatasetMusic100(Dataset):
|
||||
def __init__(self):
|
||||
Dataset.__init__(self)
|
||||
self.d, self.nt, self.nb, self.nq = 100, 0, 10**6, 10000
|
||||
self.metric = 'IP'
|
||||
self.basedir = dataset_basedir + 'music-100/'
|
||||
self.metric = "IP"
|
||||
self.basedir = dataset_basedir + "music-100/"
|
||||
|
||||
def get_queries(self):
|
||||
xq = np.fromfile(self.basedir + 'query_music100.bin', dtype='float32')
|
||||
xq = np.fromfile(self.basedir + "query_music100.bin", dtype="float32")
|
||||
xq = xq.reshape(-1, 100)
|
||||
return xq
|
||||
|
||||
def get_database(self):
|
||||
xb = np.fromfile(self.basedir + 'database_music100.bin', dtype='float32')
|
||||
xb = np.fromfile(
|
||||
self.basedir + "database_music100.bin", dtype="float32"
|
||||
)
|
||||
xb = xb.reshape(-1, 100)
|
||||
return xb
|
||||
|
||||
def get_groundtruth(self, k=None):
|
||||
gt = np.load(self.basedir + 'gt.npy')
|
||||
gt = np.load(self.basedir + "gt.npy")
|
||||
if k is not None:
|
||||
assert k <= 100
|
||||
gt = gt[:, :k]
|
||||
@@ -331,7 +356,7 @@ class DatasetGIST1M(Dataset):
|
||||
def __init__(self):
|
||||
Dataset.__init__(self)
|
||||
self.d, self.nt, self.nb, self.nq = 960, 100000, 1000000, 10000
|
||||
self.basedir = dataset_basedir + 'gist1M/'
|
||||
self.basedir = dataset_basedir + "gist1M/"
|
||||
|
||||
def get_queries(self):
|
||||
return fvecs_read(self.basedir + "gist_query.fvecs")
|
||||
@@ -350,6 +375,7 @@ class DatasetGIST1M(Dataset):
|
||||
gt = gt[:, :k]
|
||||
return gt
|
||||
|
||||
|
||||
class DatasetDINO10B(Dataset):
|
||||
"""
|
||||
Data from https://dl.fbaipublicfiles.com/large_objects/dino_vitl_10B/
|
||||
@@ -357,19 +383,52 @@ class DatasetDINO10B(Dataset):
|
||||
The dataset is sharded in multiple chunked .bvecs files. Downloading instructions can be obtained with "wget https://dl.fbaipublicfiles.com/large_objects/dino_vitl_10B/README.md".
|
||||
Supported sizes : 100k 200k 500k 1M ... 5B 10B listed in supported_nbs (see __init__).
|
||||
"""
|
||||
def __init__(self, nb, ignore_supported = False):
|
||||
|
||||
def __init__(self, nb, ignore_supported=False):
|
||||
Dataset.__init__(self)
|
||||
supported_nbs = [100_000, 200_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000, 100_000_000, 200_000_000, 500_000_000, 1_000_000_000, 2_000_000_000, 5_000_000_000, 10_000_000_000]
|
||||
supported_nbs = [
|
||||
100_000,
|
||||
200_000,
|
||||
500_000,
|
||||
1_000_000,
|
||||
2_000_000,
|
||||
5_000_000,
|
||||
10_000_000,
|
||||
20_000_000,
|
||||
50_000_000,
|
||||
100_000_000,
|
||||
200_000_000,
|
||||
500_000_000,
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
5_000_000_000,
|
||||
10_000_000_000,
|
||||
]
|
||||
if nb not in supported_nbs and not ignore_supported:
|
||||
raise ValueError(f"Unsupported dataset size: {nb}, supported values are: {supported_nbs}")
|
||||
raise ValueError(
|
||||
f"Unsupported dataset size: {nb}, supported values are: {supported_nbs}"
|
||||
)
|
||||
if not os.path.exists(dataset_basedir):
|
||||
raise ValueError(f"Provided dataset base directory does not exist: {dataset_basedir}")
|
||||
raise ValueError(
|
||||
f"Provided dataset base directory does not exist: {dataset_basedir}"
|
||||
)
|
||||
self.basedir = dataset_basedir + "dino_vitl_10B/"
|
||||
self.indexdir = self.basedir + "chunked_base_10B"
|
||||
assert os.path.exists(self.indexdir), f"Index path should exist, check your dataset path: {self.indexdir}"
|
||||
assert os.path.exists(
|
||||
self.indexdir
|
||||
), f"Index path should exist, check your dataset path: {self.indexdir}"
|
||||
self.queriesdir = self.basedir + "queries_clean.bvecs"
|
||||
assert os.path.exists(self.queriesdir), f"Queries path should exist as dataset size {nb} is supported: {self.queriesdir}"
|
||||
self.gtsdir = self.basedir + "gts/" + "gts_dino_patch_" + str(nb) + "_" + "k10.npy"
|
||||
assert os.path.exists(
|
||||
self.queriesdir
|
||||
), f"Queries path should exist as dataset size {nb} is supported: {self.queriesdir}"
|
||||
self.gtsdir = (
|
||||
self.basedir
|
||||
+ "gts/"
|
||||
+ "gts_dino_patch_"
|
||||
+ str(nb)
|
||||
+ "_"
|
||||
+ "k10.npy"
|
||||
)
|
||||
self.train_queriesdir = self.basedir + "train_queries_99M.bvecs"
|
||||
self.nb = nb
|
||||
self.d = 1024
|
||||
@@ -377,31 +436,36 @@ class DatasetDINO10B(Dataset):
|
||||
self.nt = 99_000_000
|
||||
self.metric = "L2"
|
||||
|
||||
|
||||
def get_queries(self):
|
||||
"""Get all vectors as a single array"""
|
||||
queries = bvecs_mmap(self.queriesdir)
|
||||
return sanitize(queries)
|
||||
|
||||
def get_train(self, maxtrain = None):
|
||||
def get_train(self, maxtrain=None):
|
||||
"""Get training query vectors as a single array"""
|
||||
if maxtrain is None or maxtrain > 10_000_000:
|
||||
raise NotImplementedError("The training set is potentially too large to fit in RAM (400 GB of data). Please use train_iterator or use maxtrain parameter below 10_000_000 to get the first maxtrain training vectors.")
|
||||
raise NotImplementedError(
|
||||
"The training set is potentially too large to fit in RAM (400 GB of data). Please use train_iterator or use maxtrain parameter below 10_000_000 to get the first maxtrain training vectors."
|
||||
)
|
||||
return sanitize(bvecs_mmap(self.train_queriesdir)[:maxtrain])
|
||||
|
||||
def get_database(self):
|
||||
"""Get all database vectors as a single array"""
|
||||
if self.nb > 10_000_000:
|
||||
raise NotImplementedError("The dataset is potentially too large to fit in RAM. Please use database_iterator or use a dataset size equal to or below 10_000_000.")
|
||||
raise NotImplementedError(
|
||||
"The dataset is potentially too large to fit in RAM. Please use database_iterator or use a dataset size equal to or below 10_000_000."
|
||||
)
|
||||
else:
|
||||
return sanitize(bvecs_iter_chunked(self.indexdir, batch_size=self.nb).__next__())
|
||||
return sanitize(
|
||||
bvecs_iter_chunked(self.indexdir, batch_size=self.nb).__next__()
|
||||
)
|
||||
|
||||
def database_iterator(self, bs=10_000):
|
||||
"""Iterator over the database of size nb, corresponding to the first nb vectors in the .bvecs file"""
|
||||
total_read = 0
|
||||
for batch in bvecs_iter_chunked(self.indexdir, batch_size=bs):
|
||||
if total_read + batch.shape[0] > self.nb:
|
||||
batch = batch[:self.nb - total_read]
|
||||
batch = batch[: self.nb - total_read]
|
||||
yield sanitize(batch)
|
||||
total_read += batch.shape[0]
|
||||
if total_read >= self.nb:
|
||||
@@ -412,10 +476,12 @@ class DatasetDINO10B(Dataset):
|
||||
for batch in bvecs_iter(self.train_queriesdir, batch_size=bs):
|
||||
yield sanitize(batch)
|
||||
|
||||
def get_groundtruth(self, k = 10):
|
||||
def get_groundtruth(self, k=10):
|
||||
"""Get ground truth from .npy file"""
|
||||
if k > 10:
|
||||
raise NotImplementedError("Ground truth files only available for k<=10")
|
||||
raise NotImplementedError(
|
||||
"Ground truth files only available for k<=10"
|
||||
)
|
||||
gts = np.load(self.gtsdir)
|
||||
gts = gts[:, :k]
|
||||
return gts
|
||||
@@ -423,29 +489,30 @@ class DatasetDINO10B(Dataset):
|
||||
def distance(self):
|
||||
return "euclidean"
|
||||
|
||||
def dataset_from_name(dataset='deep1M', download=False):
|
||||
""" converts a string describing a dataset to a Dataset object
|
||||
|
||||
def dataset_from_name(dataset="deep1M", download=False):
|
||||
"""converts a string describing a dataset to a Dataset object
|
||||
Supports sift1M, bigann1M..bigann1B, deep1M..deep1B, music-100 and glove
|
||||
"""
|
||||
|
||||
if dataset == 'sift1M':
|
||||
if dataset == "sift1M":
|
||||
return DatasetSIFT1M()
|
||||
|
||||
elif dataset == 'gist1M':
|
||||
elif dataset == "gist1M":
|
||||
return DatasetGIST1M()
|
||||
|
||||
elif dataset.startswith('bigann'):
|
||||
elif dataset.startswith("bigann"):
|
||||
dbsize = 1000 if dataset == "bigann1B" else int(dataset[6:-1])
|
||||
return DatasetBigANN(nb_M=dbsize)
|
||||
|
||||
elif dataset.startswith("deep"):
|
||||
|
||||
szsuf = dataset[4:]
|
||||
if szsuf[-1] == 'M':
|
||||
dbsize = 10 ** 6 * int(szsuf[:-1])
|
||||
elif szsuf == '1B':
|
||||
dbsize = 10 ** 9
|
||||
elif szsuf[-1] == 'k':
|
||||
if szsuf[-1] == "M":
|
||||
dbsize = 10**6 * int(szsuf[:-1])
|
||||
elif szsuf == "1B":
|
||||
dbsize = 10**9
|
||||
elif szsuf[-1] == "k":
|
||||
dbsize = 1000 * int(szsuf[:-1])
|
||||
else:
|
||||
assert False, "did not recognize suffix " + szsuf
|
||||
|
||||
+51
-49
@@ -15,22 +15,19 @@ from multiprocessing.pool import ThreadPool
|
||||
|
||||
|
||||
def knn_intersection_measure(I1, I2):
|
||||
""" computes the intersection measure of two result tables
|
||||
"""
|
||||
"""computes the intersection measure of two result tables"""
|
||||
nq, rank = I1.shape
|
||||
assert I2.shape == (nq, rank)
|
||||
ninter = sum(
|
||||
np.intersect1d(I1[i], I2[i]).size
|
||||
for i in range(nq)
|
||||
)
|
||||
ninter = sum(np.intersect1d(I1[i], I2[i]).size for i in range(nq))
|
||||
return ninter / I1.size
|
||||
|
||||
|
||||
###############################################################
|
||||
# Range search results can be compared with Precision-Recall
|
||||
|
||||
|
||||
def filter_range_results(lims, D, I, thresh):
|
||||
""" select a set of results """
|
||||
"""select a set of results"""
|
||||
nq = lims.size - 1
|
||||
mask = D < thresh
|
||||
new_lims = np.zeros_like(lims)
|
||||
@@ -41,13 +38,13 @@ def filter_range_results(lims, D, I, thresh):
|
||||
|
||||
def range_PR(lims_ref, Iref, lims_new, Inew, mode="overall"):
|
||||
"""compute the precision and recall of range search results. The
|
||||
function does not take the distances into account. """
|
||||
function does not take the distances into account."""
|
||||
|
||||
def ref_result_for(i):
|
||||
return Iref[lims_ref[i]:lims_ref[i + 1]]
|
||||
return Iref[lims_ref[i] : lims_ref[i + 1]]
|
||||
|
||||
def new_result_for(i):
|
||||
return Inew[lims_new[i]:lims_new[i + 1]]
|
||||
return Inew[lims_new[i] : lims_new[i + 1]]
|
||||
|
||||
nq = lims_ref.size - 1
|
||||
assert lims_new.size - 1 == nq
|
||||
@@ -75,12 +72,12 @@ def range_PR(lims_ref, Iref, lims_new, Inew, mode="overall"):
|
||||
lims_ref[1:] - lims_ref[:-1],
|
||||
lims_new[1:] - lims_new[:-1],
|
||||
ninter,
|
||||
mode=mode
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
|
||||
def counts_to_PR(ngt, nres, ninter, mode="overall"):
|
||||
""" computes a precision-recall for a set of queries.
|
||||
"""computes a precision-recall for a set of queries.
|
||||
ngt = nb of GT results per query
|
||||
nres = nb of found results per query
|
||||
ninter = nb of correct results per query (smaller than nres of course)
|
||||
@@ -127,7 +124,7 @@ def counts_to_PR(ngt, nres, ninter, mode="overall"):
|
||||
|
||||
|
||||
def sort_range_res_2(lims, D, I):
|
||||
""" sort 2 arrays using the first as key """
|
||||
"""sort 2 arrays using the first as key"""
|
||||
I2 = np.empty_like(I)
|
||||
D2 = np.empty_like(D)
|
||||
nq = len(lims) - 1
|
||||
@@ -152,12 +149,16 @@ def sort_range_res_1(lims, I):
|
||||
|
||||
|
||||
def range_PR_multiple_thresholds(
|
||||
lims_ref, Iref,
|
||||
lims_new, Dnew, Inew,
|
||||
thresholds,
|
||||
mode="overall", do_sort="ref,new"
|
||||
):
|
||||
""" compute precision-recall values for range search results
|
||||
lims_ref,
|
||||
Iref,
|
||||
lims_new,
|
||||
Dnew,
|
||||
Inew,
|
||||
thresholds,
|
||||
mode="overall",
|
||||
do_sort="ref,new",
|
||||
):
|
||||
"""compute precision-recall values for range search results
|
||||
for several thresholds on the "new" results.
|
||||
This is to plot PR curves
|
||||
"""
|
||||
@@ -170,7 +171,7 @@ def range_PR_multiple_thresholds(
|
||||
Inew, Dnew = sort_range_res_2(lims_new, Dnew, Inew)
|
||||
|
||||
def ref_result_for(i):
|
||||
return Iref[lims_ref[i]:lims_ref[i + 1]]
|
||||
return Iref[lims_ref[i] : lims_ref[i + 1]]
|
||||
|
||||
def new_result_for(i):
|
||||
l0, l1 = lims_new[i], lims_new[i + 1]
|
||||
@@ -193,7 +194,7 @@ def range_PR_multiple_thresholds(
|
||||
return
|
||||
|
||||
# which offsets we are interested in
|
||||
nres= np.searchsorted(res_dis, thresholds)
|
||||
nres = np.searchsorted(res_dis, thresholds)
|
||||
counts[q, :, 1] = nres
|
||||
|
||||
if gt_ids.size == 0:
|
||||
@@ -216,8 +217,7 @@ def range_PR_multiple_thresholds(
|
||||
recalls = np.zeros(nt)
|
||||
for t in range(nt):
|
||||
p, r = counts_to_PR(
|
||||
counts[:, t, 0], counts[:, t, 1], counts[:, t, 2],
|
||||
mode=mode
|
||||
counts[:, t, 0], counts[:, t, 1], counts[:, t, 2], mode=mode
|
||||
)
|
||||
precisions[t] = p
|
||||
recalls[t] = r
|
||||
@@ -229,28 +229,29 @@ def range_PR_multiple_thresholds(
|
||||
# Functions that compare search results with a reference result.
|
||||
# They are intended for use in tests
|
||||
|
||||
|
||||
def _cluster_tables_with_tolerance(tab1, tab2, thr):
|
||||
""" for two tables, cluster them by merging values closer than thr.
|
||||
Returns the cluster ids for each table element """
|
||||
"""for two tables, cluster them by merging values closer than thr.
|
||||
Returns the cluster ids for each table element"""
|
||||
tab = np.hstack([tab1, tab2])
|
||||
tab.sort()
|
||||
n = len(tab)
|
||||
diffs = np.ones(n)
|
||||
diffs[1:] = tab[1:] - tab[:-1]
|
||||
unique_vals = tab[diffs > thr]
|
||||
idx1 = np.searchsorted(unique_vals, tab1, side='right') - 1
|
||||
idx2 = np.searchsorted(unique_vals, tab2, side='right') - 1
|
||||
idx1 = np.searchsorted(unique_vals, tab1, side="right") - 1
|
||||
idx2 = np.searchsorted(unique_vals, tab2, side="right") - 1
|
||||
return idx1, idx2
|
||||
|
||||
|
||||
def check_ref_knn_with_draws(Dref, Iref, Dnew, Inew, rtol=1e-5):
|
||||
""" test that knn search results are identical, with possible ties.
|
||||
Raise if not. """
|
||||
"""test that knn search results are identical, with possible ties.
|
||||
Raise if not."""
|
||||
np.testing.assert_allclose(Dref, Dnew, rtol=rtol)
|
||||
# here we have to be careful because of draws
|
||||
testcase = unittest.TestCase() # because it makes nice error messages
|
||||
testcase = unittest.TestCase() # because it makes nice error messages
|
||||
for i in range(len(Iref)):
|
||||
if np.all(Iref[i] == Inew[i]): # easy case
|
||||
if np.all(Iref[i] == Inew[i]): # easy case
|
||||
continue
|
||||
|
||||
# otherwise collect elements per distance
|
||||
@@ -265,10 +266,9 @@ def check_ref_knn_with_draws(Dref, Iref, Dnew, Inew, rtol=1e-5):
|
||||
testcase.assertEqual(set(Iref[i, mask]), set(Inew[i, mask]))
|
||||
|
||||
|
||||
def check_ref_range_results(Lref, Dref, Iref,
|
||||
Lnew, Dnew, Inew):
|
||||
""" compare range search results wrt. a reference result,
|
||||
throw if it fails """
|
||||
def check_ref_range_results(Lref, Dref, Iref, Lnew, Dnew, Inew):
|
||||
"""compare range search results wrt. a reference result,
|
||||
throw if it fails"""
|
||||
np.testing.assert_array_equal(Lref, Lnew)
|
||||
nq = len(Lref) - 1
|
||||
for i in range(nq):
|
||||
@@ -277,12 +277,14 @@ def check_ref_range_results(Lref, Dref, Iref,
|
||||
Ii_new = Inew[l0:l1]
|
||||
Di_ref = Dref[l0:l1]
|
||||
Di_new = Dnew[l0:l1]
|
||||
if np.all(Ii_ref == Ii_new): # easy
|
||||
if np.all(Ii_ref == Ii_new): # easy
|
||||
pass
|
||||
else:
|
||||
|
||||
def sort_by_ids(I, D):
|
||||
o = I.argsort()
|
||||
return I[o], D[o]
|
||||
|
||||
# sort both
|
||||
(Ii_ref, Di_ref) = sort_by_ids(Ii_ref, Di_ref)
|
||||
(Ii_new, Di_new) = sort_by_ids(Ii_new, Di_new)
|
||||
@@ -309,11 +311,11 @@ class OperatingPoints:
|
||||
self.suboptimal_points = []
|
||||
|
||||
def compare_keys(self, k1, k2):
|
||||
""" return -1 if k1 > k2, 1 if k2 > k1, 0 otherwise """
|
||||
"""return -1 if k1 > k2, 1 if k2 > k1, 0 otherwise"""
|
||||
raise NotImplemented
|
||||
|
||||
def do_nothing_key(self):
|
||||
""" parameters to say we do nothing, takes 0 time and has 0 performance"""
|
||||
"""parameters to say we do nothing, takes 0 time and has 0 performance"""
|
||||
raise NotImplemented
|
||||
|
||||
def is_pareto_optimal(self, perf_new, t_new):
|
||||
@@ -323,15 +325,15 @@ class OperatingPoints:
|
||||
return True
|
||||
|
||||
def predict_bounds(self, key):
|
||||
""" predicts the bound on time and performance """
|
||||
"""predicts the bound on time and performance"""
|
||||
min_time = 0.0
|
||||
max_perf = 1.0
|
||||
for key2, perf, t in self.operating_points + self.suboptimal_points:
|
||||
cmp = self.compare_keys(key, key2)
|
||||
if cmp > 0: # key2 > key
|
||||
if cmp > 0: # key2 > key
|
||||
if t > min_time:
|
||||
min_time = t
|
||||
if cmp < 0: # key2 < key
|
||||
if cmp < 0: # key2 < key
|
||||
if perf < max_perf:
|
||||
max_perf = perf
|
||||
return max_perf, min_time
|
||||
@@ -347,8 +349,7 @@ class OperatingPoints:
|
||||
while i < len(self.operating_points):
|
||||
op_Ls, perf2, t2 = self.operating_points[i]
|
||||
if perf >= perf2 and t < t2:
|
||||
self.suboptimal_points.append(
|
||||
self.operating_points.pop(i))
|
||||
self.suboptimal_points.append(self.operating_points.pop(i))
|
||||
else:
|
||||
i += 1
|
||||
self.operating_points.append((key, perf, t))
|
||||
@@ -388,7 +389,7 @@ class OperatingPointsWithRanges(OperatingPoints):
|
||||
return int(np.prod([len(values) for name, values in self.ranges]))
|
||||
|
||||
def sample_experiments(self, n_autotune, rs=np.random):
|
||||
""" sample a set of experiments of max size n_autotune
|
||||
"""sample a set of experiments of max size n_autotune
|
||||
(run all experiments in random order if n_autotune is 0)
|
||||
"""
|
||||
assert n_autotune == 0 or n_autotune >= 2
|
||||
@@ -398,7 +399,8 @@ class OperatingPointsWithRanges(OperatingPoints):
|
||||
experiments = rs.permutation(totex - 2)
|
||||
else:
|
||||
experiments = rs.choice(
|
||||
totex - 2, size=n_autotune - 2, replace=False)
|
||||
totex - 2, size=n_autotune - 2, replace=False
|
||||
)
|
||||
|
||||
experiments = [0, totex - 1] + [int(cno) + 1 for cno in experiments]
|
||||
return experiments
|
||||
@@ -415,12 +417,11 @@ class OperatingPointsWithRanges(OperatingPoints):
|
||||
def get_parameters(self, k):
|
||||
"""Convert a key to a dictionary with parameter values"""
|
||||
return {
|
||||
name: values[k[i]]
|
||||
for i, (name, values) in enumerate(self.ranges)
|
||||
name: values[k[i]] for i, (name, values) in enumerate(self.ranges)
|
||||
}
|
||||
|
||||
def restrict_range(self, name, max_val):
|
||||
""" remove too large values from a range"""
|
||||
"""remove too large values from a range"""
|
||||
for name2, values in self.ranges:
|
||||
if name == name2:
|
||||
val2 = [v for v in values if v < max_val]
|
||||
@@ -476,6 +477,7 @@ class RepeatTimer:
|
||||
enters a loop. It focuses on ms-scale times because for second scale
|
||||
it's usually less relevant to repeat the operation.
|
||||
"""
|
||||
|
||||
def __init__(self, warmup=0, nt=-1, runs=1, max_secs=np.inf):
|
||||
assert warmup < runs
|
||||
self.warmup = warmup
|
||||
@@ -494,5 +496,5 @@ class RepeatTimer:
|
||||
return np.std(self.times) * 1000 if len(self.times) > 1 else 0.0
|
||||
|
||||
def nruns(self):
|
||||
""" effective number of runs (may be lower than runs - warmup due to timeout)"""
|
||||
"""effective number of runs (may be lower than runs - warmup due to timeout)"""
|
||||
return len(self.times)
|
||||
|
||||
@@ -12,7 +12,9 @@ import logging
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def knn_ground_truth(xq, db_iterator, k, metric_type=faiss.METRIC_L2, shard=False, ngpu=-1):
|
||||
def knn_ground_truth(
|
||||
xq, db_iterator, k, metric_type=faiss.METRIC_L2, shard=False, ngpu=-1
|
||||
):
|
||||
"""Computes the exact KNN search results for a dataset that possibly
|
||||
does not fit in RAM but for which we have an iterator that
|
||||
returns it block by block.
|
||||
@@ -28,7 +30,7 @@ def knn_ground_truth(xq, db_iterator, k, metric_type=faiss.METRIC_L2, shard=Fals
|
||||
ngpu = faiss.get_num_gpus()
|
||||
|
||||
if ngpu:
|
||||
LOG.info('running on %d GPUs' % ngpu)
|
||||
LOG.info("running on %d GPUs" % ngpu)
|
||||
co = faiss.GpuMultipleClonerOptions()
|
||||
co.shard = shard
|
||||
index = faiss.index_cpu_to_all_gpus(index, co=co, ngpu=ngpu)
|
||||
@@ -50,12 +52,11 @@ def knn_ground_truth(xq, db_iterator, k, metric_type=faiss.METRIC_L2, shard=Fals
|
||||
|
||||
return rh.D, rh.I
|
||||
|
||||
|
||||
# knn function used to be here
|
||||
knn = faiss.knn
|
||||
|
||||
|
||||
|
||||
|
||||
def range_search_gpu(xq, r2, index_gpu, index_cpu, gpu_k=1024):
|
||||
"""GPU does not support range search, so we emulate it with
|
||||
knn search + fallback to CPU index.
|
||||
@@ -71,13 +72,14 @@ def range_search_gpu(xq, r2, index_gpu, index_cpu, gpu_k=1024):
|
||||
r2 = int(r2) if is_binary_index else float(r2)
|
||||
k = min(index_gpu.ntotal, gpu_k)
|
||||
LOG.debug(
|
||||
f"GPU search {nq} queries with {k=:} {is_binary_index=:} {keep_max=:}")
|
||||
f"GPU search {nq} queries with {k=:} {is_binary_index=:} {keep_max=:}"
|
||||
)
|
||||
t0 = time.time()
|
||||
D, I = index_gpu.search(xq, k)
|
||||
t1 = time.time() - t0
|
||||
if is_binary_index:
|
||||
assert d * 8 < 32768 # let's compact the distance matrix
|
||||
D = D.astype('int16')
|
||||
D = D.astype("int16")
|
||||
t2 = 0
|
||||
lim_remain = None
|
||||
if index_cpu is not None:
|
||||
@@ -96,16 +98,19 @@ def range_search_gpu(xq, r2, index_gpu, index_cpu, gpu_k=1024):
|
||||
else:
|
||||
index_cpu = faiss.IndexFlat(d, index_gpu.metric_type)
|
||||
index_cpu.add(xb)
|
||||
lim_remain, D_remain, I_remain = index_cpu.range_search(xq[mask], r2)
|
||||
lim_remain, D_remain, I_remain = index_cpu.range_search(
|
||||
xq[mask], r2
|
||||
)
|
||||
if is_binary_index:
|
||||
D_remain = D_remain.astype('int16')
|
||||
D_remain = D_remain.astype("int16")
|
||||
t2 = time.time() - t0
|
||||
LOG.debug("combine")
|
||||
t0 = time.time()
|
||||
|
||||
CombinerRangeKNN = (
|
||||
faiss.CombinerRangeKNNint16 if is_binary_index else
|
||||
faiss.CombinerRangeKNNfloat
|
||||
faiss.CombinerRangeKNNint16
|
||||
if is_binary_index
|
||||
else faiss.CombinerRangeKNNfloat
|
||||
)
|
||||
|
||||
combiner = CombinerRangeKNN(nq, k, r2, keep_max)
|
||||
@@ -120,11 +125,11 @@ def range_search_gpu(xq, r2, index_gpu, index_cpu, gpu_k=1024):
|
||||
combiner.lim_remain = sp(lim_remain.view("int64"))
|
||||
combiner.I_remain = sp(I_remain)
|
||||
# combiner.set_range_result(sp(mask), sp(lim_remain.view("int64")), sp(D_remain), sp(I_remain))
|
||||
L_res = np.empty(nq + 1, dtype='int64')
|
||||
L_res = np.empty(nq + 1, dtype="int64")
|
||||
combiner.compute_sizes(sp(L_res))
|
||||
nres = L_res[-1]
|
||||
D_res = np.empty(nres, dtype=D.dtype)
|
||||
I_res = np.empty(nres, dtype='int64')
|
||||
I_res = np.empty(nres, dtype="int64")
|
||||
combiner.write_result(sp(D_res), sp(I_res))
|
||||
else:
|
||||
D_res, I_res = [], []
|
||||
@@ -150,21 +155,27 @@ def range_search_gpu(xq, r2, index_gpu, index_cpu, gpu_k=1024):
|
||||
return L_res, D_res, I_res
|
||||
|
||||
|
||||
def range_ground_truth(xq, db_iterator, threshold, metric_type=faiss.METRIC_L2,
|
||||
shard=False, ngpu=-1):
|
||||
def range_ground_truth(
|
||||
xq,
|
||||
db_iterator,
|
||||
threshold,
|
||||
metric_type=faiss.METRIC_L2,
|
||||
shard=False,
|
||||
ngpu=-1,
|
||||
):
|
||||
"""Computes the range-search search results for a dataset that possibly
|
||||
does not fit in RAM but for which we have an iterator that
|
||||
returns it block by block.
|
||||
"""
|
||||
nq, d = xq.shape
|
||||
t0 = time.time()
|
||||
xq = np.ascontiguousarray(xq, dtype='float32')
|
||||
xq = np.ascontiguousarray(xq, dtype="float32")
|
||||
|
||||
index = faiss.IndexFlat(d, metric_type)
|
||||
if ngpu == -1:
|
||||
ngpu = faiss.get_num_gpus()
|
||||
if ngpu:
|
||||
LOG.info('running on %d GPUs' % ngpu)
|
||||
LOG.info("running on %d GPUs" % ngpu)
|
||||
co = faiss.GpuMultipleClonerOptions()
|
||||
co.shard = shard
|
||||
index_gpu = faiss.index_cpu_to_all_gpus(index, co=co, ngpu=ngpu)
|
||||
@@ -192,8 +203,8 @@ def range_ground_truth(xq, db_iterator, threshold, metric_type=faiss.METRIC_L2,
|
||||
i0 += ni
|
||||
LOG.info("%d db elements, %.3f s" % (i0, time.time() - t0))
|
||||
|
||||
empty_I = np.zeros(0, dtype='int64')
|
||||
empty_D = np.zeros(0, dtype='float32')
|
||||
empty_I = np.zeros(0, dtype="int64")
|
||||
empty_D = np.zeros(0, dtype="float32")
|
||||
# import pdb; pdb.set_trace()
|
||||
D = [(np.hstack(i) if i != [] else empty_D) for i in D]
|
||||
I = [(np.hstack(i) if i != [] else empty_I) for i in I]
|
||||
@@ -205,7 +216,7 @@ def range_ground_truth(xq, db_iterator, threshold, metric_type=faiss.METRIC_L2,
|
||||
|
||||
|
||||
def threshold_radius_nres(nres, dis, ids, thresh, keep_max=False):
|
||||
""" select a set of results """
|
||||
"""select a set of results"""
|
||||
if keep_max:
|
||||
mask = dis > thresh
|
||||
else:
|
||||
@@ -213,14 +224,14 @@ def threshold_radius_nres(nres, dis, ids, thresh, keep_max=False):
|
||||
new_nres = np.zeros_like(nres)
|
||||
o = 0
|
||||
for i, nr in enumerate(nres):
|
||||
nr = int(nr) # avoid issues with int64 + uint64
|
||||
new_nres[i] = mask[o:o + nr].sum()
|
||||
nr = int(nr) # avoid issues with int64 + uint64
|
||||
new_nres[i] = mask[o : o + nr].sum()
|
||||
o += nr
|
||||
return new_nres, dis[mask], ids[mask]
|
||||
|
||||
|
||||
def threshold_radius(lims, dis, ids, thresh, keep_max=False):
|
||||
""" restrict range-search results to those below a given radius """
|
||||
"""restrict range-search results to those below a given radius"""
|
||||
if keep_max:
|
||||
mask = dis > thresh
|
||||
else:
|
||||
@@ -246,24 +257,32 @@ def apply_maxres(res_batches, target_nres, keep_max=False):
|
||||
alldis.partition(target_nres)
|
||||
radius = alldis[target_nres]
|
||||
|
||||
if alldis.dtype == 'float32':
|
||||
if alldis.dtype == "float32":
|
||||
radius = float(radius)
|
||||
else:
|
||||
radius = int(radius)
|
||||
LOG.debug(' setting radius to %s' % radius)
|
||||
LOG.debug(" setting radius to %s" % radius)
|
||||
totres = 0
|
||||
for i, (nres, dis, ids) in enumerate(res_batches):
|
||||
nres, dis, ids = threshold_radius_nres(
|
||||
nres, dis, ids, radius, keep_max=keep_max)
|
||||
nres, dis, ids, radius, keep_max=keep_max
|
||||
)
|
||||
totres += len(dis)
|
||||
res_batches[i] = nres, dis, ids
|
||||
LOG.debug(' updated previous results, new nb results %d' % totres)
|
||||
LOG.debug(" updated previous results, new nb results %d" % totres)
|
||||
return radius, totres
|
||||
|
||||
|
||||
def range_search_max_results(index, query_iterator, radius,
|
||||
max_results=None, min_results=None,
|
||||
shard=False, ngpu=0, clip_to_min=False):
|
||||
def range_search_max_results(
|
||||
index,
|
||||
query_iterator,
|
||||
radius,
|
||||
max_results=None,
|
||||
min_results=None,
|
||||
shard=False,
|
||||
ngpu=0,
|
||||
clip_to_min=False,
|
||||
):
|
||||
"""Performs a range search with many queries (given by an iterator)
|
||||
and adjusts the threshold on-the-fly so that the total results
|
||||
table does not grow larger than max_results.
|
||||
@@ -287,7 +306,7 @@ def range_search_max_results(index, query_iterator, radius,
|
||||
ngpu = faiss.get_num_gpus()
|
||||
|
||||
if ngpu:
|
||||
LOG.info('running on %d GPUs' % ngpu)
|
||||
LOG.info("running on %d GPUs" % ngpu)
|
||||
co = faiss.GpuMultipleClonerOptions()
|
||||
co.shard = shard
|
||||
index_gpu = faiss.index_cpu_to_all_gpus(index, co=co, ngpu=ngpu)
|
||||
@@ -314,54 +333,60 @@ def range_search_max_results(index, query_iterator, radius,
|
||||
t1 = time.time()
|
||||
if is_binary_index:
|
||||
# weird Faiss quirk that returns floats for Hamming distances
|
||||
Di = Di.astype('int16')
|
||||
Di = Di.astype("int16")
|
||||
|
||||
totres += len(Di)
|
||||
res_batches.append((nres_i, Di, Ii))
|
||||
|
||||
if max_results is not None and totres > max_results:
|
||||
LOG.info('too many results %d > %d, scaling back radius' %
|
||||
(totres, max_results))
|
||||
LOG.info(
|
||||
"too many results %d > %d, scaling back radius"
|
||||
% (totres, max_results)
|
||||
)
|
||||
radius, totres = apply_maxres(
|
||||
res_batches, min_results,
|
||||
keep_max=index.metric_type == faiss.METRIC_INNER_PRODUCT
|
||||
res_batches,
|
||||
min_results,
|
||||
keep_max=index.metric_type == faiss.METRIC_INNER_PRODUCT,
|
||||
)
|
||||
t2 = time.time()
|
||||
t_search += t1 - t0
|
||||
t_post_process += t2 - t1
|
||||
LOG.debug(' [%.3f s] %d queries done, %d results' % (
|
||||
time.time() - t_start, qtot, totres))
|
||||
LOG.debug(
|
||||
" [%.3f s] %d queries done, %d results"
|
||||
% (time.time() - t_start, qtot, totres)
|
||||
)
|
||||
|
||||
LOG.info(
|
||||
'search done in %.3f s + %.3f s, total %d results, end threshold %g' % (
|
||||
t_search, t_post_process, totres, radius)
|
||||
"search done in %.3f s + %.3f s, total %d results, end threshold %g"
|
||||
% (t_search, t_post_process, totres, radius)
|
||||
)
|
||||
|
||||
if clip_to_min and totres > min_results:
|
||||
radius, totres = apply_maxres(
|
||||
res_batches, min_results,
|
||||
keep_max=index.metric_type == faiss.METRIC_INNER_PRODUCT
|
||||
res_batches,
|
||||
min_results,
|
||||
keep_max=index.metric_type == faiss.METRIC_INNER_PRODUCT,
|
||||
)
|
||||
|
||||
nres = np.hstack([nres_i for nres_i, dis_i, ids_i in res_batches])
|
||||
dis = np.hstack([dis_i for nres_i, dis_i, ids_i in res_batches])
|
||||
ids = np.hstack([ids_i for nres_i, dis_i, ids_i in res_batches])
|
||||
|
||||
lims = np.zeros(len(nres) + 1, dtype='uint64')
|
||||
lims = np.zeros(len(nres) + 1, dtype="uint64")
|
||||
lims[1:] = np.cumsum(nres)
|
||||
|
||||
return radius, lims, dis, ids
|
||||
|
||||
|
||||
def exponential_query_iterator(xq, start_bs=32, max_bs=20000):
|
||||
""" produces batches of progressively increasing sizes. This is useful to
|
||||
"""produces batches of progressively increasing sizes. This is useful to
|
||||
adjust the search radius progressively without overflowing with
|
||||
intermediate results """
|
||||
intermediate results"""
|
||||
nq = len(xq)
|
||||
bs = start_bs
|
||||
i = 0
|
||||
while i < nq:
|
||||
xqi = xq[i:i + bs]
|
||||
xqi = xq[i : i + bs]
|
||||
yield xqi
|
||||
if bs < max_bs:
|
||||
bs *= 2
|
||||
|
||||
+21
-17
@@ -8,14 +8,14 @@ import re
|
||||
|
||||
|
||||
def get_code_size(d, indexkey):
|
||||
""" size of one vector in an index in dimension d
|
||||
"""size of one vector in an index in dimension d
|
||||
constructed with factory string indexkey"""
|
||||
|
||||
if indexkey == "Flat":
|
||||
return d * 4
|
||||
|
||||
if indexkey.endswith(",RFlat"):
|
||||
return d * 4 + get_code_size(d, indexkey[:-len(",RFlat")])
|
||||
return d * 4 + get_code_size(d, indexkey[: -len(",RFlat")])
|
||||
|
||||
mo = re.match("IVF\\d+(_HNSW32)?,(.*)$", indexkey)
|
||||
if mo:
|
||||
@@ -33,42 +33,42 @@ def get_code_size(d, indexkey):
|
||||
if mo:
|
||||
return get_code_size(d, mo.group(1)) + get_code_size(d, mo.group(2))
|
||||
|
||||
mo = re.match('PQ(\\d+)x(\\d+)(fs|fsr)?$', indexkey)
|
||||
mo = re.match("PQ(\\d+)x(\\d+)(fs|fsr)?$", indexkey)
|
||||
if mo:
|
||||
return (int(mo.group(1)) * int(mo.group(2)) + 7) // 8
|
||||
|
||||
mo = re.match('PQ(\\d+)\\+(\\d+)$', indexkey)
|
||||
mo = re.match("PQ(\\d+)\\+(\\d+)$", indexkey)
|
||||
if mo:
|
||||
return (int(mo.group(1)) + int(mo.group(2)))
|
||||
return int(mo.group(1)) + int(mo.group(2))
|
||||
|
||||
mo = re.match('PQ(\\d+)$', indexkey)
|
||||
mo = re.match("PQ(\\d+)$", indexkey)
|
||||
if mo:
|
||||
return int(mo.group(1))
|
||||
|
||||
if indexkey == "HNSW32" or indexkey == "HNSW32,Flat":
|
||||
return d * 4 + 64 * 4 # roughly
|
||||
return d * 4 + 64 * 4 # roughly
|
||||
|
||||
if indexkey == 'SQ8':
|
||||
if indexkey == "SQ8":
|
||||
return d
|
||||
elif indexkey == 'SQ4':
|
||||
elif indexkey == "SQ4":
|
||||
return (d + 1) // 2
|
||||
elif indexkey == 'SQ6':
|
||||
elif indexkey == "SQ6":
|
||||
return (d * 6 + 7) // 8
|
||||
elif indexkey == 'SQfp16':
|
||||
elif indexkey == "SQfp16":
|
||||
return d * 2
|
||||
elif indexkey == 'SQbf16':
|
||||
elif indexkey == "SQbf16":
|
||||
return d * 2
|
||||
|
||||
mo = re.match('PCAR?(\\d+),(.*)$', indexkey)
|
||||
mo = re.match("PCAR?(\\d+),(.*)$", indexkey)
|
||||
if mo:
|
||||
return get_code_size(int(mo.group(1)), mo.group(2))
|
||||
mo = re.match('OPQ\\d+_(\\d+),(.*)$', indexkey)
|
||||
mo = re.match("OPQ\\d+_(\\d+),(.*)$", indexkey)
|
||||
if mo:
|
||||
return get_code_size(int(mo.group(1)), mo.group(2))
|
||||
mo = re.match('OPQ\\d+,(.*)$', indexkey)
|
||||
mo = re.match("OPQ\\d+,(.*)$", indexkey)
|
||||
if mo:
|
||||
return get_code_size(d, mo.group(1))
|
||||
mo = re.match('RR(\\d+),(.*)$', indexkey)
|
||||
mo = re.match("RR(\\d+),(.*)$", indexkey)
|
||||
if mo:
|
||||
return get_code_size(int(mo.group(1)), mo.group(2))
|
||||
raise RuntimeError("cannot parse " + indexkey)
|
||||
@@ -158,7 +158,11 @@ def reverse_index_factory(index):
|
||||
return f"PQ{index.pq.M}x{index.pq.nbits}"
|
||||
|
||||
elif isinstance(index, faiss.IndexLSH):
|
||||
return "LSH" + ("r" if index.rotate_data else "") + ("t" if index.train_thresholds else "")
|
||||
return (
|
||||
"LSH"
|
||||
+ ("r" if index.rotate_data else "")
|
||||
+ ("t" if index.train_thresholds else "")
|
||||
)
|
||||
|
||||
elif isinstance(index, faiss.IndexScalarQuantizer):
|
||||
return sq_names[index.sq.qtype]
|
||||
|
||||
+24
-31
@@ -8,12 +8,12 @@ import faiss
|
||||
|
||||
|
||||
def get_invlist(invlists, l):
|
||||
""" returns the inverted lists content as a pair of (list_ids, list_codes).
|
||||
"""returns the inverted lists content as a pair of (list_ids, list_codes).
|
||||
The codes are reshaped to a proper size
|
||||
"""
|
||||
invlists = faiss.downcast_InvertedLists(invlists)
|
||||
ls = invlists.list_size(l)
|
||||
list_ids = np.zeros(ls, dtype='int64')
|
||||
list_ids = np.zeros(ls, dtype="int64")
|
||||
ids = codes = None
|
||||
try:
|
||||
ids = invlists.get_ids(l)
|
||||
@@ -21,13 +21,13 @@ def get_invlist(invlists, l):
|
||||
faiss.memcpy(faiss.swig_ptr(list_ids), ids, list_ids.nbytes)
|
||||
codes = invlists.get_codes(l)
|
||||
if invlists.code_size != faiss.InvertedLists.INVALID_CODE_SIZE:
|
||||
list_codes = np.zeros((ls, invlists.code_size), dtype='uint8')
|
||||
list_codes = np.zeros((ls, invlists.code_size), dtype="uint8")
|
||||
else:
|
||||
# it's a BlockInvertedLists
|
||||
npb = invlists.n_per_block
|
||||
bs = invlists.block_size
|
||||
ls_round = (ls + npb - 1) // npb
|
||||
list_codes = np.zeros((ls_round, bs // npb, npb), dtype='uint8')
|
||||
list_codes = np.zeros((ls_round, bs // npb, npb), dtype="uint8")
|
||||
if ls > 0:
|
||||
faiss.memcpy(faiss.swig_ptr(list_codes), codes, list_codes.nbytes)
|
||||
finally:
|
||||
@@ -39,28 +39,27 @@ def get_invlist(invlists, l):
|
||||
|
||||
|
||||
def get_invlist_sizes(invlists):
|
||||
""" return the array of sizes of the inverted lists """
|
||||
return np.array([
|
||||
invlists.list_size(i)
|
||||
for i in range(invlists.nlist)
|
||||
], dtype='int64')
|
||||
"""return the array of sizes of the inverted lists"""
|
||||
return np.array(
|
||||
[invlists.list_size(i) for i in range(invlists.nlist)], dtype="int64"
|
||||
)
|
||||
|
||||
|
||||
def print_object_fields(obj):
|
||||
""" list values all fields of an object known to SWIG """
|
||||
"""list values all fields of an object known to SWIG"""
|
||||
|
||||
for name in obj.__class__.__swig_getmethods__:
|
||||
print(f"{name} = {getattr(obj, name)}")
|
||||
|
||||
|
||||
def get_pq_centroids(pq):
|
||||
""" return the PQ centroids as an array """
|
||||
"""return the PQ centroids as an array"""
|
||||
cen = faiss.vector_to_array(pq.centroids)
|
||||
return cen.reshape(pq.M, pq.ksub, pq.dsub)
|
||||
|
||||
|
||||
def get_LinearTransform_matrix(pca):
|
||||
""" extract matrix + bias from the PCA object
|
||||
"""extract matrix + bias from the PCA object
|
||||
works for any linear transform (OPQ, random rotation, etc.)
|
||||
"""
|
||||
b = faiss.vector_to_array(pca.b)
|
||||
@@ -69,10 +68,10 @@ def get_LinearTransform_matrix(pca):
|
||||
|
||||
|
||||
def make_LinearTransform_matrix(A, b=None):
|
||||
""" make a linear transform from a matrix and a bias term (optional)"""
|
||||
"""make a linear transform from a matrix and a bias term (optional)"""
|
||||
d_out, d_in = A.shape
|
||||
if b is not None:
|
||||
assert b.shape == (d_out, )
|
||||
assert b.shape == (d_out,)
|
||||
lt = faiss.LinearTransform(d_in, d_out, b is not None)
|
||||
faiss.copy_array_to_vector(A.ravel(), lt.A)
|
||||
if b is not None:
|
||||
@@ -83,35 +82,29 @@ def make_LinearTransform_matrix(A, b=None):
|
||||
|
||||
|
||||
def get_additive_quantizer_codebooks(aq):
|
||||
""" return to codebooks of an additive quantizer """
|
||||
"""return to codebooks of an additive quantizer"""
|
||||
codebooks = faiss.vector_to_array(aq.codebooks).reshape(-1, aq.d)
|
||||
co = faiss.vector_to_array(aq.codebook_offsets)
|
||||
return [
|
||||
codebooks[co[i]:co[i + 1]]
|
||||
for i in range(aq.M)
|
||||
]
|
||||
return [codebooks[co[i] : co[i + 1]] for i in range(aq.M)]
|
||||
|
||||
|
||||
def get_flat_data(index):
|
||||
""" copy and return the data matrix in an IndexFlat """
|
||||
"""copy and return the data matrix in an IndexFlat"""
|
||||
xb = faiss.vector_to_array(index.codes).view("float32")
|
||||
return xb.reshape(index.ntotal, index.d)
|
||||
|
||||
|
||||
def get_flat_codes(index_flat):
|
||||
""" get the codes from an indexFlatCodes as an array """
|
||||
def get_flat_codes(index_flat):
|
||||
"""get the codes from an indexFlatCodes as an array"""
|
||||
return faiss.vector_to_array(index_flat.codes).reshape(
|
||||
index_flat.ntotal, index_flat.code_size)
|
||||
index_flat.ntotal, index_flat.code_size
|
||||
)
|
||||
|
||||
|
||||
def get_NSG_neighbors(nsg):
|
||||
""" get the neighbor list for the vectors stored in the NSG structure, as
|
||||
a N-by-K matrix of indices """
|
||||
"""get the neighbor list for the vectors stored in the NSG structure, as
|
||||
a N-by-K matrix of indices"""
|
||||
graph = nsg.get_final_graph()
|
||||
neighbors = np.zeros((graph.N, graph.K), dtype='int32')
|
||||
faiss.memcpy(
|
||||
faiss.swig_ptr(neighbors),
|
||||
graph.data,
|
||||
neighbors.nbytes
|
||||
)
|
||||
neighbors = np.zeros((graph.N, graph.K), dtype="int32")
|
||||
faiss.memcpy(faiss.swig_ptr(neighbors), graph.data, neighbors.nbytes)
|
||||
return neighbors
|
||||
|
||||
+8
-12
@@ -14,16 +14,14 @@ def add_preassigned(index_ivf, x, a, ids=None):
|
||||
Add elements to an IVF index, where the assignment is already computed
|
||||
"""
|
||||
n, d = x.shape
|
||||
assert a.shape == (n, )
|
||||
assert a.shape == (n,)
|
||||
if isinstance(index_ivf, faiss.IndexBinaryIVF):
|
||||
d *= 8
|
||||
assert d == index_ivf.d
|
||||
if ids is not None:
|
||||
assert ids.shape == (n, )
|
||||
assert ids.shape == (n,)
|
||||
ids = faiss.swig_ptr(ids)
|
||||
index_ivf.add_core(
|
||||
n, faiss.swig_ptr(x), ids, faiss.swig_ptr(a)
|
||||
)
|
||||
index_ivf.add_core(n, faiss.swig_ptr(x), ids, faiss.swig_ptr(a))
|
||||
|
||||
|
||||
def search_preassigned(index_ivf, xq, k, list_nos, coarse_dis=None):
|
||||
@@ -83,9 +81,7 @@ def range_search_preassigned(index_ivf, x, radius, list_nos, coarse_dis=None):
|
||||
sp = faiss.swig_ptr
|
||||
|
||||
index_ivf.range_search_preassigned_c(
|
||||
n, sp(x), radius,
|
||||
sp(list_nos), sp(coarse_dis),
|
||||
res
|
||||
n, sp(x), radius, sp(list_nos), sp(coarse_dis), res
|
||||
)
|
||||
# get pointers and copy them
|
||||
lims = faiss.rev_swig_ptr(res.lims, n + 1).copy()
|
||||
@@ -96,7 +92,7 @@ def range_search_preassigned(index_ivf, x, radius, list_nos, coarse_dis=None):
|
||||
|
||||
|
||||
def replace_ivf_quantizer(index_ivf, new_quantizer):
|
||||
""" replace the IVF quantizer with a flat quantizer and return the
|
||||
"""replace the IVF quantizer with a flat quantizer and return the
|
||||
old quantizer"""
|
||||
if new_quantizer.ntotal == 0:
|
||||
centroids = index_ivf.quantizer.reconstruct_n()
|
||||
@@ -120,15 +116,15 @@ def replace_ivf_quantizer(index_ivf, new_quantizer):
|
||||
|
||||
|
||||
def permute_invlists(index_ivf, perm):
|
||||
""" Apply some permutation to the inverted lists, and modify the quantizer
|
||||
"""Apply some permutation to the inverted lists, and modify the quantizer
|
||||
entries accordingly.
|
||||
Perm is an array of size nlist, where old_index = perm[new_index]
|
||||
"""
|
||||
nlist, = perm.shape
|
||||
(nlist,) = perm.shape
|
||||
assert index_ivf.nlist == nlist
|
||||
quantizer = faiss.downcast_index(index_ivf.quantizer)
|
||||
assert quantizer.ntotal == index_ivf.nlist
|
||||
perm = np.ascontiguousarray(perm, dtype='int64')
|
||||
perm = np.ascontiguousarray(perm, dtype="int64")
|
||||
|
||||
# just make sure it's a permutation...
|
||||
bc = np.bincount(perm, minlength=nlist)
|
||||
|
||||
+7
-2
@@ -11,7 +11,10 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def merge_ondisk(
|
||||
trained_index: faiss.Index, shard_fnames: List[str], ivfdata_fname: str, shift_ids=False
|
||||
trained_index: faiss.Index,
|
||||
shard_fnames: List[str],
|
||||
ivfdata_fname: str,
|
||||
shift_ids=False,
|
||||
) -> None:
|
||||
"""Add the contents of the indexes stored in shard_fnames into the index
|
||||
trained_index. The on-disk data is stored in ivfdata_fname"""
|
||||
@@ -51,7 +54,9 @@ def merge_ondisk(
|
||||
ivf_vector.push_back(ivf)
|
||||
|
||||
LOG.info("merge %d inverted lists " % ivf_vector.size())
|
||||
ntotal = invlists.merge_from_multiple(ivf_vector.data(), ivf_vector.size(), shift_ids)
|
||||
ntotal = invlists.merge_from_multiple(
|
||||
ivf_vector.data(), ivf_vector.size(), shift_ids
|
||||
)
|
||||
|
||||
# now replace the inverted lists in the output index
|
||||
index.ntotal = index_ivf.ntotal = ntotal
|
||||
|
||||
+55
-50
@@ -27,8 +27,8 @@ LOG = logging.getLogger(__name__)
|
||||
PORT = 12032
|
||||
|
||||
safe_modules = {
|
||||
'numpy',
|
||||
'numpy.core.multiarray',
|
||||
"numpy",
|
||||
"numpy.core.multiarray",
|
||||
}
|
||||
|
||||
|
||||
@@ -39,49 +39,51 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
if module in safe_modules:
|
||||
return getattr(importlib.import_module(module), name)
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
(module, name))
|
||||
raise pickle.UnpicklingError(
|
||||
"global '%s.%s' is forbidden" % (module, name)
|
||||
)
|
||||
|
||||
|
||||
class FileSock:
|
||||
" wraps a socket so that it is usable by pickle/cPickle "
|
||||
"wraps a socket so that it is usable by pickle/cPickle"
|
||||
|
||||
def __init__(self,sock):
|
||||
def __init__(self, sock):
|
||||
self.sock = sock
|
||||
self.nr=0
|
||||
self.nr = 0
|
||||
|
||||
def write(self, buf):
|
||||
# print("sending %d bytes"%len(buf))
|
||||
#self.sock.sendall(buf)
|
||||
# self.sock.sendall(buf)
|
||||
# print("...done")
|
||||
bs = 512 * 1024
|
||||
ns = 0
|
||||
while ns < len(buf):
|
||||
sent = self.sock.send(buf[ns:ns + bs])
|
||||
sent = self.sock.send(buf[ns : ns + bs])
|
||||
ns += sent
|
||||
|
||||
def read(self,bs=512*1024):
|
||||
#if self.nr==10000: pdb.set_trace()
|
||||
self.nr+=1
|
||||
def read(self, bs=512 * 1024):
|
||||
# if self.nr==10000: pdb.set_trace()
|
||||
self.nr += 1
|
||||
# print("read bs=%d"%bs)
|
||||
b = []
|
||||
nb = 0
|
||||
while len(b)<bs:
|
||||
while len(b) < bs:
|
||||
# print(' loop')
|
||||
rb = self.sock.recv(bs - nb)
|
||||
if not rb: break
|
||||
if not rb:
|
||||
break
|
||||
b.append(rb)
|
||||
nb += len(rb)
|
||||
return b''.join(b)
|
||||
return b"".join(b)
|
||||
|
||||
def readline(self):
|
||||
# print("readline!")
|
||||
"""may be optimized..."""
|
||||
s=bytes()
|
||||
s = bytes()
|
||||
while True:
|
||||
c=self.read(1)
|
||||
s+=c
|
||||
if len(c)==0 or chr(c[0])=='\n':
|
||||
c = self.read(1)
|
||||
s += c
|
||||
if len(c) == 0 or chr(c[0]) == "\n":
|
||||
return s
|
||||
|
||||
|
||||
@@ -99,7 +101,7 @@ class Server:
|
||||
transparently from a client
|
||||
"""
|
||||
|
||||
def __init__(self, s, logf=sys.stderr, log_prefix=''):
|
||||
def __init__(self, s, logf=sys.stderr, log_prefix=""):
|
||||
self.logf = logf
|
||||
self.log_prefix = log_prefix
|
||||
|
||||
@@ -108,7 +110,6 @@ class Server:
|
||||
self.conn = s
|
||||
self.fs = FileSock(s)
|
||||
|
||||
|
||||
def log(self, s):
|
||||
self.logf.write("Server log %s: %s\n" % (self.log_prefix, s))
|
||||
|
||||
@@ -130,13 +131,13 @@ class Server:
|
||||
(fname, args) = RestrictedUnpickler(self.fs).load()
|
||||
except EOFError:
|
||||
raise ClientExit("read args")
|
||||
self.log("executing method %s"%(fname))
|
||||
self.log("executing method %s" % (fname))
|
||||
st = None
|
||||
ret = None
|
||||
try:
|
||||
f=getattr(self,fname)
|
||||
f = getattr(self, fname)
|
||||
except AttributeError:
|
||||
st = AttributeError("unknown method "+fname)
|
||||
st = AttributeError("unknown method " + fname)
|
||||
self.log("unknown method")
|
||||
|
||||
try:
|
||||
@@ -145,38 +146,38 @@ class Server:
|
||||
# due to a bug (in mod_python?), ServerException cannot be
|
||||
# unpickled, so send the string and make the exception on the client side
|
||||
|
||||
#st=ServerException(
|
||||
# st=ServerException(
|
||||
# "".join(traceback.format_tb(sys.exc_info()[2]))+
|
||||
# str(e))
|
||||
st="".join(traceback.format_tb(sys.exc_info()[2]))+str(e)
|
||||
st = "".join(traceback.format_tb(sys.exc_info()[2])) + str(e)
|
||||
self.log("exception in method")
|
||||
traceback.print_exc(50,self.logf)
|
||||
traceback.print_exc(50, self.logf)
|
||||
self.logf.flush()
|
||||
|
||||
LOG.info("return")
|
||||
try:
|
||||
pickle.dump((st ,ret), self.fs, protocol=4)
|
||||
pickle.dump((st, ret), self.fs, protocol=4)
|
||||
except EOFError:
|
||||
raise ClientExit("function return")
|
||||
|
||||
def exec_loop(self):
|
||||
""" main execution loop. Loops and handles exit states"""
|
||||
"""main execution loop. Loops and handles exit states"""
|
||||
|
||||
self.log("in exec_loop")
|
||||
try:
|
||||
while True:
|
||||
self.one_function()
|
||||
except ClientExit as e:
|
||||
self.log("ClientExit %s"%e)
|
||||
self.log("ClientExit %s" % e)
|
||||
except socket.error as e:
|
||||
self.log("socket error %s"%e)
|
||||
traceback.print_exc(50,self.logf)
|
||||
self.log("socket error %s" % e)
|
||||
traceback.print_exc(50, self.logf)
|
||||
except EOFError:
|
||||
self.log("EOF during communication")
|
||||
traceback.print_exc(50,self.logf)
|
||||
traceback.print_exc(50, self.logf)
|
||||
except BaseException:
|
||||
# unexpected
|
||||
traceback.print_exc(50,sys.stderr)
|
||||
traceback.print_exc(50, sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
LOG.info("exit server")
|
||||
@@ -188,14 +189,16 @@ class Server:
|
||||
# spying stuff
|
||||
|
||||
def get_ps_stats(self):
|
||||
ret=''
|
||||
f=os.popen("echo ============ `hostname` uptime:; uptime;"+
|
||||
"echo ============ self:; "+
|
||||
"ps -p %d -o pid,vsize,rss,%%cpu,nlwp,psr; "%os.getpid()+
|
||||
"echo ============ run queue:;"+
|
||||
"ps ar -o user,pid,%cpu,%mem,ni,nlwp,psr,vsz,rss,cputime,command")
|
||||
ret = ""
|
||||
f = os.popen(
|
||||
"echo ============ `hostname` uptime:; uptime;"
|
||||
+ "echo ============ self:; "
|
||||
+ "ps -p %d -o pid,vsize,rss,%%cpu,nlwp,psr; " % os.getpid()
|
||||
+ "echo ============ run queue:;"
|
||||
+ "ps ar -o user,pid,%cpu,%mem,ni,nlwp,psr,vsz,rss,cputime,command"
|
||||
)
|
||||
for l in f:
|
||||
ret+=l
|
||||
ret += l
|
||||
return ret
|
||||
|
||||
|
||||
@@ -204,6 +207,7 @@ class Client:
|
||||
Methods of the server object can be called transparently. Exceptions are
|
||||
re-raised.
|
||||
"""
|
||||
|
||||
def __init__(self, HOST, port=PORT, v6=False):
|
||||
socktype = socket.AF_INET6 if v6 else socket.AF_INET
|
||||
|
||||
@@ -220,18 +224,18 @@ class Client:
|
||||
|
||||
def get_result(self):
|
||||
(st, ret) = RestrictedUnpickler(self.fs).load()
|
||||
if st!=None:
|
||||
if st != None:
|
||||
raise ServerException(st)
|
||||
else:
|
||||
return ret
|
||||
|
||||
def __getattr__(self,name):
|
||||
return lambda *x: self.generic_fun(name,x)
|
||||
def __getattr__(self, name):
|
||||
return lambda *x: self.generic_fun(name, x)
|
||||
|
||||
|
||||
def run_server(new_handler, port=PORT, report_to_file=None, v6=False):
|
||||
|
||||
HOST = '' # Symbolic name meaning the local host
|
||||
HOST = "" # Symbolic name meaning the local host
|
||||
socktype = socket.AF_INET6 if v6 else socket.AF_INET
|
||||
s = socket.socket(socktype, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
@@ -242,20 +246,21 @@ def run_server(new_handler, port=PORT, report_to_file=None, v6=False):
|
||||
|
||||
LOG.info("accepting connections")
|
||||
if report_to_file is not None:
|
||||
LOG.info('storing host+port in %s', report_to_file)
|
||||
open(report_to_file, 'w').write('%s:%d ' % (socket.gethostname(), port))
|
||||
LOG.info("storing host+port in %s", report_to_file)
|
||||
open(report_to_file, "w").write("%s:%d " % (socket.gethostname(), port))
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn, addr = s.accept()
|
||||
except socket.error as e:
|
||||
if e[1]=='Interrupted system call': continue
|
||||
if e[1] == "Interrupted system call":
|
||||
continue
|
||||
raise
|
||||
|
||||
LOG.info('Connected to %s', addr)
|
||||
LOG.info("Connected to %s", addr)
|
||||
|
||||
ibs = new_handler(conn)
|
||||
|
||||
tid = _thread.start_new_thread(ibs.exec_loop,())
|
||||
tid = _thread.start_new_thread(ibs.exec_loop, ())
|
||||
|
||||
LOG.debug("Thread ID: %d", tid)
|
||||
|
||||
@@ -14,7 +14,6 @@ import torch
|
||||
from faiss.contrib.clustering import kmeans # noqa: F401 some libraries import kmeans from here
|
||||
|
||||
|
||||
|
||||
class DatasetAssign:
|
||||
"""Wrapper for a tensor that offers a function to assign the vectors
|
||||
to centroids. All other implementations offer the same interface"""
|
||||
|
||||
@@ -11,6 +11,7 @@ import torch
|
||||
import faiss
|
||||
import math
|
||||
from faiss.contrib.torch import clustering
|
||||
|
||||
# the kmeans can produce both torch and numpy centroids
|
||||
|
||||
|
||||
@@ -57,7 +58,7 @@ class VectorQuantizer(Quantizer):
|
||||
|
||||
class ProductQuantizer(Quantizer):
|
||||
def __init__(self, d, M, nbits):
|
||||
""" M: number of subvectors, d%M == 0
|
||||
"""M: number of subvectors, d%M == 0
|
||||
nbits: number of bits that each vector is encoded into
|
||||
"""
|
||||
assert d % M == 0
|
||||
@@ -69,20 +70,20 @@ class ProductQuantizer(Quantizer):
|
||||
self.code_size = code_size
|
||||
|
||||
def train(self, x):
|
||||
nc = 2 ** self.nbits
|
||||
nc = 2**self.nbits
|
||||
sd = self.d // self.M
|
||||
dev = x.device
|
||||
dtype = x.dtype
|
||||
self.codebook = torch.zeros((self.M, nc, sd), device=dev, dtype=dtype)
|
||||
for m in range(self.M):
|
||||
xsub = x[:, m * self.d // self.M: (m + 1) * self.d // self.M]
|
||||
xsub = x[:, m * self.d // self.M : (m + 1) * self.d // self.M]
|
||||
data = clustering.DatasetAssign(xsub.contiguous())
|
||||
self.codebook[m] = clustering.kmeans(2 ** self.nbits, data)
|
||||
self.codebook[m] = clustering.kmeans(2**self.nbits, data)
|
||||
|
||||
def encode(self, x):
|
||||
codes = torch.zeros((x.shape[0], self.code_size), dtype=torch.uint8)
|
||||
for m in range(self.M):
|
||||
xsub = x[:, m * self.d // self.M:(m + 1) * self.d // self.M]
|
||||
xsub = x[:, m * self.d // self.M : (m + 1) * self.d // self.M]
|
||||
_, I = faiss.knn(xsub.contiguous(), self.codebook[m], 1)
|
||||
codes[:, m] = I.ravel()
|
||||
return codes
|
||||
|
||||
+187
-138
@@ -34,52 +34,59 @@ import numpy as np
|
||||
|
||||
|
||||
def swig_ptr_from_UInt8Tensor(x):
|
||||
""" gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU) """
|
||||
"""gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU)"""
|
||||
assert x.is_contiguous()
|
||||
assert x.dtype == torch.uint8
|
||||
return faiss.cast_integer_to_uint8_ptr(
|
||||
x.untyped_storage().data_ptr() + x.storage_offset())
|
||||
x.untyped_storage().data_ptr() + x.storage_offset()
|
||||
)
|
||||
|
||||
|
||||
def swig_ptr_from_HalfTensor(x):
|
||||
""" gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU) """
|
||||
"""gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU)"""
|
||||
assert x.is_contiguous()
|
||||
assert x.dtype == torch.float16
|
||||
# no canonical half type in C/C++
|
||||
return faiss.cast_integer_to_void_ptr(
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 2)
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 2
|
||||
)
|
||||
|
||||
|
||||
def swig_ptr_from_FloatTensor(x):
|
||||
""" gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU) """
|
||||
"""gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU)"""
|
||||
assert x.is_contiguous()
|
||||
assert x.dtype == torch.float32
|
||||
return faiss.cast_integer_to_float_ptr(
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 4)
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 4
|
||||
)
|
||||
|
||||
|
||||
def swig_ptr_from_BFloat16Tensor(x):
|
||||
""" gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU) """
|
||||
"""gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU)"""
|
||||
assert x.is_contiguous()
|
||||
assert x.dtype == torch.bfloat16
|
||||
return faiss.cast_integer_to_void_ptr(
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 2)
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 2
|
||||
)
|
||||
|
||||
|
||||
def swig_ptr_from_IntTensor(x):
|
||||
""" gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU) """
|
||||
"""gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU)"""
|
||||
assert x.is_contiguous()
|
||||
assert x.dtype == torch.int32, 'dtype=%s' % x.dtype
|
||||
assert x.dtype == torch.int32, "dtype=%s" % x.dtype
|
||||
return faiss.cast_integer_to_int_ptr(
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 4)
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 4
|
||||
)
|
||||
|
||||
|
||||
def swig_ptr_from_IndicesTensor(x):
|
||||
""" gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU) """
|
||||
"""gets a Faiss SWIG pointer from a pytorch tensor (on CPU or GPU)"""
|
||||
assert x.is_contiguous()
|
||||
assert x.dtype == torch.int64, 'dtype=%s' % x.dtype
|
||||
assert x.dtype == torch.int64, "dtype=%s" % x.dtype
|
||||
return faiss.cast_integer_to_idx_t_ptr(
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 8)
|
||||
x.untyped_storage().data_ptr() + x.storage_offset() * 8
|
||||
)
|
||||
|
||||
|
||||
##################################################################
|
||||
# utilities
|
||||
@@ -88,10 +95,10 @@ def swig_ptr_from_IndicesTensor(x):
|
||||
|
||||
@contextlib.contextmanager
|
||||
def using_stream(res, pytorch_stream=None):
|
||||
""" Creates a scoping object to make Faiss GPU use the same stream
|
||||
as pytorch, based on torch.cuda.current_stream().
|
||||
Or, a specific pytorch stream can be passed in as a second
|
||||
argument, in which case we will use that stream.
|
||||
"""Creates a scoping object to make Faiss GPU use the same stream
|
||||
as pytorch, based on torch.cuda.current_stream().
|
||||
Or, a specific pytorch stream can be passed in as a second
|
||||
argument, in which case we will use that stream.
|
||||
"""
|
||||
|
||||
if pytorch_stream is None:
|
||||
@@ -99,7 +106,8 @@ def using_stream(res, pytorch_stream=None):
|
||||
|
||||
# This is the cudaStream_t that we wish to use
|
||||
cuda_stream_s = faiss.cast_integer_to_cudastream_t(
|
||||
pytorch_stream.cuda_stream)
|
||||
pytorch_stream.cuda_stream
|
||||
)
|
||||
|
||||
# So we can revert GpuResources stream state upon exit
|
||||
prior_dev = torch.cuda.current_device()
|
||||
@@ -114,23 +122,25 @@ def using_stream(res, pytorch_stream=None):
|
||||
res.setDefaultStream(prior_dev, prior_stream)
|
||||
|
||||
|
||||
def torch_replace_method(the_class, name, replacement,
|
||||
ignore_missing=False, ignore_no_base=False):
|
||||
def torch_replace_method(
|
||||
the_class, name, replacement, ignore_missing=False, ignore_no_base=False
|
||||
):
|
||||
try:
|
||||
orig_method = getattr(the_class, name)
|
||||
except AttributeError:
|
||||
if ignore_missing:
|
||||
return
|
||||
raise
|
||||
if orig_method.__name__ == 'torch_replacement_' + name:
|
||||
if orig_method.__name__ == "torch_replacement_" + name:
|
||||
# replacement was done in parent class
|
||||
return
|
||||
|
||||
# We should already have the numpy replacement methods patched
|
||||
assert ignore_no_base or (orig_method.__name__ == 'replacement_' + name)
|
||||
setattr(the_class, name + '_numpy', orig_method)
|
||||
assert ignore_no_base or (orig_method.__name__ == "replacement_" + name)
|
||||
setattr(the_class, name + "_numpy", orig_method)
|
||||
setattr(the_class, name, replacement)
|
||||
|
||||
|
||||
##################################################################
|
||||
# Setup wrappers
|
||||
##################################################################
|
||||
@@ -155,8 +165,9 @@ def handle_torch_Index(the_class):
|
||||
)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -165,8 +176,9 @@ def handle_torch_Index(the_class):
|
||||
# CPU torch
|
||||
self.add_ex(n, x_ptr, numeric_type)
|
||||
|
||||
def torch_replacement_add_with_ids(self, x, ids,
|
||||
numeric_type=faiss.Float32):
|
||||
def torch_replacement_add_with_ids(
|
||||
self, x, ids, numeric_type=faiss.Float32
|
||||
):
|
||||
if type(x) is np.ndarray:
|
||||
# forward to faiss __init__.py base method
|
||||
return self.add_with_ids_numpy(x, ids)
|
||||
@@ -184,12 +196,13 @@ def handle_torch_Index(the_class):
|
||||
)
|
||||
|
||||
assert type(ids) is torch.Tensor
|
||||
assert ids.shape == (n, ), 'not same number of vectors as ids'
|
||||
assert ids.shape == (n,), "not same number of vectors as ids"
|
||||
ids_ptr = swig_ptr_from_IndicesTensor(ids)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -216,8 +229,9 @@ def handle_torch_Index(the_class):
|
||||
L_ptr = swig_ptr_from_IndicesTensor(labels)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -246,8 +260,9 @@ def handle_torch_Index(the_class):
|
||||
)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -283,8 +298,9 @@ def handle_torch_Index(the_class):
|
||||
|
||||
return x_ptr, D_ptr, I_ptr, D, I
|
||||
|
||||
def torch_replacement_search(self, x, k, D=None, I=None,
|
||||
numeric_type=faiss.Float32):
|
||||
def torch_replacement_search(
|
||||
self, x, k, D=None, I=None, numeric_type=faiss.Float32
|
||||
):
|
||||
if type(x) is np.ndarray:
|
||||
# forward to faiss __init__.py base method
|
||||
return self.search_numpy(x, k, D=D, I=I)
|
||||
@@ -296,8 +312,9 @@ def handle_torch_Index(the_class):
|
||||
x_ptr, D_ptr, I_ptr, D, I = search_methods_common(x, k, D, I)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -308,8 +325,9 @@ def handle_torch_Index(the_class):
|
||||
|
||||
return D, I
|
||||
|
||||
def torch_replacement_search_and_reconstruct(self, x, k, D=None, I=None,
|
||||
R=None):
|
||||
def torch_replacement_search_and_reconstruct(
|
||||
self, x, k, D=None, I=None, R=None
|
||||
):
|
||||
if type(x) is np.ndarray:
|
||||
# Forward to faiss __init__.py base method
|
||||
return self.search_and_reconstruct_numpy(x, k, D=D, I=I, R=R)
|
||||
@@ -328,21 +346,22 @@ def handle_torch_Index(the_class):
|
||||
R_ptr = swig_ptr_from_FloatTensor(R)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
self.search_and_reconstruct_c(n, x_ptr, k, D_ptr, I_ptr, R_ptr)
|
||||
else:
|
||||
# CPU torch
|
||||
self.search_and_reconstruct_c(n, x_ptr, k, D_ptr, I_ptr,
|
||||
R_ptr)
|
||||
self.search_and_reconstruct_c(n, x_ptr, k, D_ptr, I_ptr, R_ptr)
|
||||
|
||||
return D, I, R
|
||||
|
||||
def torch_replacement_search_preassigned(self, x, k, Iq, Dq, *,
|
||||
D=None, I=None):
|
||||
def torch_replacement_search_preassigned(
|
||||
self, x, k, Iq, Dq, *, D=None, I=None
|
||||
):
|
||||
if type(x) is np.ndarray:
|
||||
# forward to faiss __init__.py base method
|
||||
return self.search_preassigned_numpy(x, k, Iq, Dq, D=D, I=I)
|
||||
@@ -365,24 +384,28 @@ def handle_torch_Index(the_class):
|
||||
Dq_ptr = None
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
self.search_preassigned_c(n, x_ptr, k, Iq_ptr, Dq_ptr,
|
||||
D_ptr, I_ptr, False)
|
||||
self.search_preassigned_c(
|
||||
n, x_ptr, k, Iq_ptr, Dq_ptr, D_ptr, I_ptr, False
|
||||
)
|
||||
else:
|
||||
# CPU torch
|
||||
self.search_preassigned_c(n, x_ptr, k, Iq_ptr, Dq_ptr,
|
||||
D_ptr, I_ptr, False)
|
||||
self.search_preassigned_c(
|
||||
n, x_ptr, k, Iq_ptr, Dq_ptr, D_ptr, I_ptr, False
|
||||
)
|
||||
|
||||
return D, I
|
||||
|
||||
def torch_replacement_remove_ids(self, x):
|
||||
# Not yet implemented
|
||||
assert type(x) is not torch.Tensor, \
|
||||
'remove_ids not yet implemented for torch'
|
||||
assert (
|
||||
type(x) is not torch.Tensor
|
||||
), "remove_ids not yet implemented for torch"
|
||||
return self.remove_ids_numpy(x)
|
||||
|
||||
def torch_replacement_reconstruct(self, key, x=None):
|
||||
@@ -395,21 +418,22 @@ def handle_torch_Index(the_class):
|
||||
|
||||
# If the index is a CPU index, the default device is CPU, otherwise we
|
||||
# produce a GPU tensor
|
||||
device = torch.device('cpu')
|
||||
if hasattr(self, 'getDevice'):
|
||||
device = torch.device("cpu")
|
||||
if hasattr(self, "getDevice"):
|
||||
# same device as the index
|
||||
device = torch.device('cuda', self.getDevice())
|
||||
device = torch.device("cuda", self.getDevice())
|
||||
|
||||
if x is None:
|
||||
x = torch.empty(self.d, device=device, dtype=torch.float32)
|
||||
else:
|
||||
assert type(x) is torch.Tensor
|
||||
assert x.shape == (self.d, )
|
||||
assert x.shape == (self.d,)
|
||||
x_ptr = swig_ptr_from_FloatTensor(x)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -433,10 +457,10 @@ def handle_torch_Index(the_class):
|
||||
|
||||
# If the index is a CPU index, the default device is CPU, otherwise we
|
||||
# produce a GPU tensor
|
||||
device = torch.device('cpu')
|
||||
if hasattr(self, 'getDevice'):
|
||||
device = torch.device("cpu")
|
||||
if hasattr(self, "getDevice"):
|
||||
# same device as the index
|
||||
device = torch.device('cuda', self.getDevice())
|
||||
device = torch.device("cuda", self.getDevice())
|
||||
|
||||
if x is None:
|
||||
x = torch.empty(ni, self.d, device=device, dtype=torch.float32)
|
||||
@@ -446,8 +470,9 @@ def handle_torch_Index(the_class):
|
||||
x_ptr = swig_ptr_from_FloatTensor(x)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -464,7 +489,7 @@ def handle_torch_Index(the_class):
|
||||
return self.update_vectors_numpy(keys, x)
|
||||
|
||||
assert type(keys) is torch.Tensor
|
||||
(n, ) = keys.shape
|
||||
(n,) = keys.shape
|
||||
keys_ptr = swig_ptr_from_IndicesTensor(keys)
|
||||
|
||||
assert type(x) is torch.Tensor
|
||||
@@ -472,8 +497,9 @@ def handle_torch_Index(the_class):
|
||||
x_ptr = swig_ptr_from_FloatTensor(x)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -494,10 +520,12 @@ def handle_torch_Index(the_class):
|
||||
assert d == self.d
|
||||
x_ptr = swig_ptr_from_FloatTensor(x)
|
||||
|
||||
assert not x.is_cuda, \
|
||||
'Range search using GPU tensor not yet implemented'
|
||||
assert not hasattr(self, 'getDevice'), \
|
||||
'Range search on GPU index not yet implemented'
|
||||
assert (
|
||||
not x.is_cuda
|
||||
), "Range search using GPU tensor not yet implemented"
|
||||
assert not hasattr(
|
||||
self, "getDevice"
|
||||
), "Range search on GPU index not yet implemented"
|
||||
|
||||
res = faiss.RangeSearchResult(n)
|
||||
self.range_search_c(n, x_ptr, thresh, res)
|
||||
@@ -507,7 +535,8 @@ def handle_torch_Index(the_class):
|
||||
# np to torch
|
||||
# NOTE: torch does not support np.uint64, just np.int64
|
||||
lims = torch.from_numpy(
|
||||
faiss.rev_swig_ptr(res.lims, n + 1).copy().astype('int64'))
|
||||
faiss.rev_swig_ptr(res.lims, n + 1).copy().astype("int64")
|
||||
)
|
||||
nd = int(lims[-1])
|
||||
D = torch.from_numpy(faiss.rev_swig_ptr(res.distances, nd).copy())
|
||||
I = torch.from_numpy(faiss.rev_swig_ptr(res.labels, nd).copy())
|
||||
@@ -531,8 +560,9 @@ def handle_torch_Index(the_class):
|
||||
codes_ptr = swig_ptr_from_UInt8Tensor(codes)
|
||||
|
||||
if x.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -561,8 +591,9 @@ def handle_torch_Index(the_class):
|
||||
x_ptr = swig_ptr_from_FloatTensor(x)
|
||||
|
||||
if codes.is_cuda:
|
||||
assert hasattr(self, 'getDevice'), \
|
||||
'GPU tensor on CPU index not allowed'
|
||||
assert hasattr(
|
||||
self, "getDevice"
|
||||
), "GPU tensor on CPU index not allowed"
|
||||
|
||||
# On the GPU, use proper stream ordering
|
||||
with using_stream(self.getResources()):
|
||||
@@ -573,34 +604,46 @@ def handle_torch_Index(the_class):
|
||||
|
||||
return x
|
||||
|
||||
torch_replace_method(the_class, 'add', torch_replacement_add)
|
||||
torch_replace_method(the_class, 'add_with_ids',
|
||||
torch_replacement_add_with_ids)
|
||||
torch_replace_method(the_class, 'assign', torch_replacement_assign)
|
||||
torch_replace_method(the_class, 'train', torch_replacement_train)
|
||||
torch_replace_method(the_class, 'search', torch_replacement_search)
|
||||
torch_replace_method(the_class, 'remove_ids',
|
||||
torch_replacement_remove_ids)
|
||||
torch_replace_method(the_class, 'reconstruct',
|
||||
torch_replacement_reconstruct)
|
||||
torch_replace_method(the_class, 'reconstruct_n',
|
||||
torch_replacement_reconstruct_n)
|
||||
torch_replace_method(the_class, 'range_search',
|
||||
torch_replacement_range_search)
|
||||
torch_replace_method(the_class, 'update_vectors',
|
||||
torch_replacement_update_vectors,
|
||||
ignore_missing=True)
|
||||
torch_replace_method(the_class, 'search_and_reconstruct',
|
||||
torch_replacement_search_and_reconstruct,
|
||||
ignore_missing=True)
|
||||
torch_replace_method(the_class, 'search_preassigned',
|
||||
torch_replacement_search_preassigned,
|
||||
ignore_missing=True)
|
||||
torch_replace_method(the_class, 'sa_encode', torch_replacement_sa_encode)
|
||||
torch_replace_method(the_class, 'sa_decode', torch_replacement_sa_decode)
|
||||
torch_replace_method(the_class, "add", torch_replacement_add)
|
||||
torch_replace_method(
|
||||
the_class, "add_with_ids", torch_replacement_add_with_ids
|
||||
)
|
||||
torch_replace_method(the_class, "assign", torch_replacement_assign)
|
||||
torch_replace_method(the_class, "train", torch_replacement_train)
|
||||
torch_replace_method(the_class, "search", torch_replacement_search)
|
||||
torch_replace_method(the_class, "remove_ids", torch_replacement_remove_ids)
|
||||
torch_replace_method(
|
||||
the_class, "reconstruct", torch_replacement_reconstruct
|
||||
)
|
||||
torch_replace_method(
|
||||
the_class, "reconstruct_n", torch_replacement_reconstruct_n
|
||||
)
|
||||
torch_replace_method(
|
||||
the_class, "range_search", torch_replacement_range_search
|
||||
)
|
||||
torch_replace_method(
|
||||
the_class,
|
||||
"update_vectors",
|
||||
torch_replacement_update_vectors,
|
||||
ignore_missing=True,
|
||||
)
|
||||
torch_replace_method(
|
||||
the_class,
|
||||
"search_and_reconstruct",
|
||||
torch_replacement_search_and_reconstruct,
|
||||
ignore_missing=True,
|
||||
)
|
||||
torch_replace_method(
|
||||
the_class,
|
||||
"search_preassigned",
|
||||
torch_replacement_search_preassigned,
|
||||
ignore_missing=True,
|
||||
)
|
||||
torch_replace_method(the_class, "sa_encode", torch_replacement_sa_encode)
|
||||
torch_replace_method(the_class, "sa_decode", torch_replacement_sa_decode)
|
||||
|
||||
|
||||
faiss_module = sys.modules['faiss']
|
||||
faiss_module = sys.modules["faiss"]
|
||||
|
||||
# Re-patch anything that inherits from faiss.Index to add the torch bindings
|
||||
for symbol in dir(faiss_module):
|
||||
@@ -615,8 +658,7 @@ for symbol in dir(faiss_module):
|
||||
def torch_replacement_knn(xq, xb, k, metric=faiss.METRIC_L2, metric_arg=0):
|
||||
if type(xb) is np.ndarray:
|
||||
# Forward to faiss __init__.py base method
|
||||
return faiss.knn_numpy(xq, xb, k, metric=metric,
|
||||
metric_arg=metric_arg)
|
||||
return faiss.knn_numpy(xq, xb, k, metric=metric, metric_arg=metric_arg)
|
||||
|
||||
nb, d = xb.size()
|
||||
assert xb.is_contiguous()
|
||||
@@ -637,31 +679,32 @@ def torch_replacement_knn(xq, xb, k, metric=faiss.METRIC_L2, metric_arg=0):
|
||||
xq_ptr = swig_ptr_from_FloatTensor(xq)
|
||||
|
||||
if metric == faiss.METRIC_L2:
|
||||
faiss.knn_L2sqr(
|
||||
xq_ptr, xb_ptr,
|
||||
d, nq, nb, k, D_ptr, I_ptr
|
||||
)
|
||||
faiss.knn_L2sqr(xq_ptr, xb_ptr, d, nq, nb, k, D_ptr, I_ptr)
|
||||
elif metric == faiss.METRIC_INNER_PRODUCT:
|
||||
faiss.knn_inner_product(
|
||||
xq_ptr, xb_ptr,
|
||||
d, nq, nb, k, D_ptr, I_ptr
|
||||
)
|
||||
faiss.knn_inner_product(xq_ptr, xb_ptr, d, nq, nb, k, D_ptr, I_ptr)
|
||||
else:
|
||||
faiss.knn_extra_metrics(
|
||||
xq_ptr, xb_ptr,
|
||||
d, nq, nb, metric, metric_arg, k, D_ptr, I_ptr
|
||||
xq_ptr, xb_ptr, d, nq, nb, metric, metric_arg, k, D_ptr, I_ptr
|
||||
)
|
||||
|
||||
return D, I
|
||||
|
||||
|
||||
torch_replace_method(faiss_module, 'knn', torch_replacement_knn, True, True)
|
||||
torch_replace_method(faiss_module, "knn", torch_replacement_knn, True, True)
|
||||
|
||||
|
||||
# allows torch tensor usage with bfKnn
|
||||
def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
metric=faiss.METRIC_L2, device=-1,
|
||||
use_cuvs=False):
|
||||
def torch_replacement_knn_gpu(
|
||||
res,
|
||||
xq,
|
||||
xb,
|
||||
k,
|
||||
D=None,
|
||||
I=None,
|
||||
metric=faiss.METRIC_L2,
|
||||
device=-1,
|
||||
use_cuvs=False,
|
||||
):
|
||||
if type(xb) is np.ndarray:
|
||||
# Forward to faiss __init__.py base method
|
||||
return faiss.knn_gpu_numpy(res, xq, xb, k, D, I, metric, device)
|
||||
@@ -673,7 +716,7 @@ def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
xb = xb.t()
|
||||
xb_row_major = False
|
||||
else:
|
||||
raise TypeError('matrix should be row or column-major')
|
||||
raise TypeError("matrix should be row or column-major")
|
||||
|
||||
if xb.dtype == torch.float32:
|
||||
xb_type = faiss.DistanceDataType_F32
|
||||
@@ -685,7 +728,7 @@ def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
xb_type = faiss.DistanceDataType_BF16
|
||||
xb_ptr = swig_ptr_from_BFloat16Tensor(xb)
|
||||
else:
|
||||
raise TypeError('xq must be float32, float16 or bfloat16')
|
||||
raise TypeError("xq must be float32, float16 or bfloat16")
|
||||
|
||||
nq, d2 = xq.size()
|
||||
assert d2 == d
|
||||
@@ -695,7 +738,7 @@ def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
xq = xq.t()
|
||||
xq_row_major = False
|
||||
else:
|
||||
raise TypeError('matrix should be row or column-major')
|
||||
raise TypeError("matrix should be row or column-major")
|
||||
|
||||
if xq.dtype == torch.float32:
|
||||
xq_type = faiss.DistanceDataType_F32
|
||||
@@ -707,14 +750,14 @@ def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
xq_type = faiss.DistanceDataType_BF16
|
||||
xq_ptr = swig_ptr_from_BFloat16Tensor(xq)
|
||||
else:
|
||||
raise TypeError('xq must be float32, float16 or bfloat16')
|
||||
raise TypeError("xq must be float32, float16 or bfloat16")
|
||||
|
||||
if D is None:
|
||||
D = torch.empty(nq, k, device=xb.device, dtype=torch.float32)
|
||||
else:
|
||||
assert D.shape == (nq, k)
|
||||
# interface takes void*, we need to check this
|
||||
assert (D.dtype == torch.float32)
|
||||
assert D.dtype == torch.float32
|
||||
|
||||
if I is None:
|
||||
I = torch.empty(nq, k, device=xb.device, dtype=torch.int64)
|
||||
@@ -728,7 +771,7 @@ def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
I_type = faiss.IndicesDataType_I32
|
||||
I_ptr = swig_ptr_from_IntTensor(I)
|
||||
else:
|
||||
raise TypeError('I must be i64 or i32')
|
||||
raise TypeError("I must be i64 or i32")
|
||||
|
||||
D_ptr = swig_ptr_from_FloatTensor(D)
|
||||
|
||||
@@ -756,14 +799,15 @@ def torch_replacement_knn_gpu(res, xq, xb, k, D=None, I=None,
|
||||
return D, I
|
||||
|
||||
|
||||
torch_replace_method(faiss_module, 'knn_gpu', torch_replacement_knn_gpu,
|
||||
True, True)
|
||||
torch_replace_method(
|
||||
faiss_module, "knn_gpu", torch_replacement_knn_gpu, True, True
|
||||
)
|
||||
|
||||
|
||||
# allows torch tensor usage with bfKnn for all pairwise distances
|
||||
def torch_replacement_pairwise_distance_gpu(res, xq, xb, D=None,
|
||||
metric=faiss.METRIC_L2,
|
||||
device=-1):
|
||||
def torch_replacement_pairwise_distance_gpu(
|
||||
res, xq, xb, D=None, metric=faiss.METRIC_L2, device=-1
|
||||
):
|
||||
if type(xb) is np.ndarray:
|
||||
# Forward to faiss __init__.py base method
|
||||
return faiss.pairwise_distance_gpu_numpy(res, xq, xb, D, metric)
|
||||
@@ -775,7 +819,7 @@ def torch_replacement_pairwise_distance_gpu(res, xq, xb, D=None,
|
||||
xb = xb.t()
|
||||
xb_row_major = False
|
||||
else:
|
||||
raise TypeError('xb matrix should be row or column-major')
|
||||
raise TypeError("xb matrix should be row or column-major")
|
||||
|
||||
if xb.dtype == torch.float32:
|
||||
xb_type = faiss.DistanceDataType_F32
|
||||
@@ -784,7 +828,7 @@ def torch_replacement_pairwise_distance_gpu(res, xq, xb, D=None,
|
||||
xb_type = faiss.DistanceDataType_F16
|
||||
xb_ptr = swig_ptr_from_HalfTensor(xb)
|
||||
else:
|
||||
raise TypeError('xb must be float32 or float16')
|
||||
raise TypeError("xb must be float32 or float16")
|
||||
|
||||
nq, d2 = xq.size()
|
||||
assert d2 == d
|
||||
@@ -794,7 +838,7 @@ def torch_replacement_pairwise_distance_gpu(res, xq, xb, D=None,
|
||||
xq = xq.t()
|
||||
xq_row_major = False
|
||||
else:
|
||||
raise TypeError('xq matrix should be row or column-major')
|
||||
raise TypeError("xq matrix should be row or column-major")
|
||||
|
||||
if xq.dtype == torch.float32:
|
||||
xq_type = faiss.DistanceDataType_F32
|
||||
@@ -803,14 +847,14 @@ def torch_replacement_pairwise_distance_gpu(res, xq, xb, D=None,
|
||||
xq_type = faiss.DistanceDataType_F16
|
||||
xq_ptr = swig_ptr_from_HalfTensor(xq)
|
||||
else:
|
||||
raise TypeError('xq must be float32 or float16')
|
||||
raise TypeError("xq must be float32 or float16")
|
||||
|
||||
if D is None:
|
||||
D = torch.empty(nq, nb, device=xb.device, dtype=torch.float32)
|
||||
else:
|
||||
assert D.shape == (nq, nb)
|
||||
# interface takes void*, we need to check this
|
||||
assert (D.dtype == torch.float32)
|
||||
assert D.dtype == torch.float32
|
||||
|
||||
D_ptr = swig_ptr_from_FloatTensor(D)
|
||||
|
||||
@@ -835,5 +879,10 @@ def torch_replacement_pairwise_distance_gpu(res, xq, xb, D=None,
|
||||
return D
|
||||
|
||||
|
||||
torch_replace_method(faiss_module, 'pairwise_distance_gpu',
|
||||
torch_replacement_pairwise_distance_gpu, True, True)
|
||||
torch_replace_method(
|
||||
faiss_module,
|
||||
"pairwise_distance_gpu",
|
||||
torch_replacement_pairwise_distance_gpu,
|
||||
True,
|
||||
True,
|
||||
)
|
||||
|
||||
+29
-23
@@ -14,51 +14,52 @@ definition of the formats here: http://corpus-texmex.irisa.fr/
|
||||
|
||||
|
||||
def ivecs_read(fname):
|
||||
a = np.fromfile(fname, dtype='int32')
|
||||
if sys.byteorder == 'big':
|
||||
a = np.fromfile(fname, dtype="int32")
|
||||
if sys.byteorder == "big":
|
||||
a.byteswap(inplace=True)
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:].copy()
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
def ivecs_mmap(fname):
|
||||
assert sys.byteorder != 'big'
|
||||
a = np.memmap(fname, dtype='int32', mode='r')
|
||||
assert sys.byteorder != "big"
|
||||
a = np.memmap(fname, dtype="int32", mode="r")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:]
|
||||
|
||||
|
||||
def fvecs_mmap(fname):
|
||||
return ivecs_mmap(fname).view('float32')
|
||||
return ivecs_mmap(fname).view("float32")
|
||||
|
||||
|
||||
def bvecs_mmap(fname):
|
||||
x = np.memmap(fname, dtype='uint8', mode='r')
|
||||
if sys.byteorder == 'big':
|
||||
x = np.memmap(fname, dtype="uint8", mode="r")
|
||||
if sys.byteorder == "big":
|
||||
da = x[:4][::-1].copy()
|
||||
d = da.view('int32')[0]
|
||||
d = da.view("int32")[0]
|
||||
else:
|
||||
d = x[:4].view('int32')[0]
|
||||
d = x[:4].view("int32")[0]
|
||||
return x.reshape(-1, d + 4)[:, 4:]
|
||||
|
||||
|
||||
def ivecs_write(fname, m):
|
||||
n, d = m.shape
|
||||
m1 = np.empty((n, d + 1), dtype='int32')
|
||||
m1 = np.empty((n, d + 1), dtype="int32")
|
||||
m1[:, 0] = d
|
||||
m1[:, 1:] = m
|
||||
if sys.byteorder == 'big':
|
||||
if sys.byteorder == "big":
|
||||
m1.byteswap(inplace=True)
|
||||
m1.tofile(fname)
|
||||
|
||||
|
||||
def fvecs_write(fname, m):
|
||||
m = m.astype('float32')
|
||||
ivecs_write(fname, m.view('int32'))
|
||||
m = m.astype("float32")
|
||||
ivecs_write(fname, m.view("int32"))
|
||||
|
||||
|
||||
def bvecs_iter(filepath, batch_size=100_000):
|
||||
"""
|
||||
@@ -66,19 +67,20 @@ def bvecs_iter(filepath, batch_size=100_000):
|
||||
"""
|
||||
|
||||
file_size = os.path.getsize(filepath)
|
||||
with open(filepath, 'rb') as f:
|
||||
dim = np.frombuffer(f.read(4), dtype='<i4')[0]
|
||||
with open(filepath, "rb") as f:
|
||||
dim = np.frombuffer(f.read(4), dtype="<i4")[0]
|
||||
|
||||
bytes_per_vec = 4 + dim
|
||||
n_vectors = file_size // bytes_per_vec
|
||||
|
||||
mm = np.memmap(filepath, mode='r', dtype=np.uint8)
|
||||
mm = np.memmap(filepath, mode="r", dtype=np.uint8)
|
||||
records = mm.reshape(n_vectors, bytes_per_vec)
|
||||
|
||||
for start in range(0, n_vectors, batch_size):
|
||||
end = np.min([start + batch_size, n_vectors])
|
||||
yield records[start:end, 4:]
|
||||
|
||||
|
||||
def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
|
||||
"""
|
||||
Memory-mapped iterator over chunked .bvecs files.
|
||||
@@ -99,7 +101,11 @@ def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
|
||||
# Find all chunk files and sort them
|
||||
chunk_files = []
|
||||
for entry in os.scandir(chunk_folder):
|
||||
if entry.is_file() and entry.name.startswith("chunk_") and entry.name.endswith(".bvecs"):
|
||||
if (
|
||||
entry.is_file()
|
||||
and entry.name.startswith("chunk_")
|
||||
and entry.name.endswith(".bvecs")
|
||||
):
|
||||
chunk_files.append(entry.path)
|
||||
chunk_files.sort()
|
||||
|
||||
@@ -111,7 +117,7 @@ def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
|
||||
for path in chunk_files:
|
||||
basename = os.path.basename(path)
|
||||
try:
|
||||
num_str = basename.split('_')[1].split('.')[0]
|
||||
num_str = basename.split("_")[1].split(".")[0]
|
||||
chunk_numbers.append(int(num_str))
|
||||
except (IndexError, ValueError):
|
||||
raise ValueError(f"Invalid chunk filename format: {basename}")
|
||||
@@ -127,8 +133,8 @@ def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
|
||||
)
|
||||
|
||||
# Get dimension from first chunk
|
||||
with open(chunk_files[0], 'rb') as f:
|
||||
dim = np.frombuffer(f.read(4), dtype='<i4')[0]
|
||||
with open(chunk_files[0], "rb") as f:
|
||||
dim = np.frombuffer(f.read(4), dtype="<i4")[0]
|
||||
|
||||
bytes_per_vec = 4 + dim
|
||||
|
||||
@@ -142,7 +148,7 @@ def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
|
||||
n_vectors = file_size // bytes_per_vec
|
||||
|
||||
# Memory-map this chunk
|
||||
mm = np.memmap(chunk_path, mode='r', dtype=np.uint8)
|
||||
mm = np.memmap(chunk_path, mode="r", dtype=np.uint8)
|
||||
records = mm.reshape(n_vectors, bytes_per_vec)
|
||||
vectors = records[:, 4:] # Skip dimension prefix
|
||||
|
||||
@@ -166,7 +172,7 @@ def bvecs_iter_chunked(chunk_folder, batch_size=100_000):
|
||||
|
||||
# Now process complete batches from this chunk
|
||||
while start + batch_size <= n_vectors:
|
||||
yield vectors[start:start + batch_size]
|
||||
yield vectors[start : start + batch_size]
|
||||
start += batch_size
|
||||
|
||||
remainder = n_vectors - start
|
||||
|
||||
+44
-26
@@ -10,8 +10,10 @@ import numpy as np
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
|
||||
matplotlib.use("Agg")
|
||||
from matplotlib import pyplot
|
||||
|
||||
graphical_output = True
|
||||
except ImportError:
|
||||
graphical_output = False
|
||||
@@ -30,15 +32,17 @@ def ivecs_read(fname):
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
def plot_OperatingPoints(ops, nq, **kwargs):
|
||||
ops = ops.optimal_pts
|
||||
n = ops.size() * 2 - 1
|
||||
pyplot.plot([ops.at( i // 2).perf for i in range(n)],
|
||||
[ops.at((i + 1) // 2).t / nq * 1000 for i in range(n)],
|
||||
**kwargs)
|
||||
pyplot.plot(
|
||||
[ops.at(i // 2).perf for i in range(n)],
|
||||
[ops.at((i + 1) // 2).t / nq * 1000 for i in range(n)],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -46,7 +50,6 @@ def plot_OperatingPoints(ops, nq, **kwargs):
|
||||
#################################################################
|
||||
|
||||
|
||||
|
||||
t0 = time.time()
|
||||
|
||||
print("load data")
|
||||
@@ -60,7 +63,7 @@ d = xt.shape[1]
|
||||
print("load GT")
|
||||
|
||||
gt = ivecs_read("sift1M/sift_groundtruth.ivecs")
|
||||
gt = gt.astype('int64')
|
||||
gt = gt.astype("int64")
|
||||
k = gt.shape[1]
|
||||
|
||||
print("prepare criterion")
|
||||
@@ -72,28 +75,41 @@ crit.nnn = k
|
||||
|
||||
# indexes that are useful when there is no limitation on memory usage
|
||||
unlimited_mem_keys = [
|
||||
"IMI2x10,Flat", "IMI2x11,Flat",
|
||||
"IVF4096,Flat", "IVF16384,Flat",
|
||||
"PCA64,IMI2x10,Flat"]
|
||||
"IMI2x10,Flat",
|
||||
"IMI2x11,Flat",
|
||||
"IVF4096,Flat",
|
||||
"IVF16384,Flat",
|
||||
"PCA64,IMI2x10,Flat",
|
||||
]
|
||||
|
||||
# memory limited to 16 bytes / vector
|
||||
keys_mem_16 = [
|
||||
'IMI2x10,PQ16', 'IVF4096,PQ16',
|
||||
'IMI2x10,PQ8+8', 'OPQ16_64,IMI2x10,PQ16'
|
||||
]
|
||||
"IMI2x10,PQ16",
|
||||
"IVF4096,PQ16",
|
||||
"IMI2x10,PQ8+8",
|
||||
"OPQ16_64,IMI2x10,PQ16",
|
||||
]
|
||||
|
||||
# limited to 32 bytes / vector
|
||||
keys_mem_32 = [
|
||||
'IMI2x10,PQ32', 'IVF4096,PQ32', 'IVF16384,PQ32',
|
||||
'IMI2x10,PQ16+16',
|
||||
'OPQ32,IVF4096,PQ32', 'IVF4096,PQ16+16', 'OPQ16,IMI2x10,PQ16+16'
|
||||
]
|
||||
"IMI2x10,PQ32",
|
||||
"IVF4096,PQ32",
|
||||
"IVF16384,PQ32",
|
||||
"IMI2x10,PQ16+16",
|
||||
"OPQ32,IVF4096,PQ32",
|
||||
"IVF4096,PQ16+16",
|
||||
"OPQ16,IMI2x10,PQ16+16",
|
||||
]
|
||||
|
||||
# indexes that can run on the GPU
|
||||
keys_gpu = [
|
||||
"PCA64,IVF4096,Flat",
|
||||
"PCA64,Flat", "Flat", "IVF4096,Flat", "IVF16384,Flat",
|
||||
"IVF4096,PQ32"]
|
||||
"PCA64,Flat",
|
||||
"Flat",
|
||||
"IVF4096,Flat",
|
||||
"IVF16384,Flat",
|
||||
"IVF4096,PQ32",
|
||||
]
|
||||
|
||||
|
||||
keys_to_test = unlimited_mem_keys
|
||||
@@ -102,8 +118,9 @@ use_gpu = False
|
||||
|
||||
if use_gpu:
|
||||
# if this fails, it means that the GPU version was not compiled
|
||||
assert faiss.StandardGpuResources, \
|
||||
"Faiss was not compiled with GPU support, or loading _swigfaiss_gpu.so failed"
|
||||
assert (
|
||||
faiss.StandardGpuResources
|
||||
), "Faiss was not compiled with GPU support, or loading _swigfaiss_gpu.so failed"
|
||||
res = faiss.StandardGpuResources()
|
||||
dev_no = 0
|
||||
|
||||
@@ -122,7 +139,6 @@ for index_key in keys_to_test:
|
||||
# make the index described by the key
|
||||
index = faiss.index_factory(d, index_key)
|
||||
|
||||
|
||||
if use_gpu:
|
||||
# transfer to GPU (may be partial)
|
||||
index = faiss.index_cpu_to_gpu(res, dev_no, index)
|
||||
@@ -155,14 +171,16 @@ for index_key in keys_to_test:
|
||||
|
||||
fig = pyplot.figure(figsize=(12, 9))
|
||||
pyplot.xlabel("1-recall at 1")
|
||||
pyplot.ylabel("search time (ms/query, %d threads)" % faiss.omp_get_max_threads())
|
||||
pyplot.gca().set_yscale('log')
|
||||
pyplot.ylabel(
|
||||
"search time (ms/query, %d threads)" % faiss.omp_get_max_threads()
|
||||
)
|
||||
pyplot.gca().set_yscale("log")
|
||||
pyplot.grid()
|
||||
for i2, opi2 in op_per_key:
|
||||
plot_OperatingPoints(opi2, crit.nq, label = i2, marker = 'o')
|
||||
plot_OperatingPoints(opi2, crit.nq, label=i2, marker="o")
|
||||
# plot_OperatingPoints(op, crit.nq, label = 'best', marker = 'o', color = 'r')
|
||||
pyplot.legend(loc=2)
|
||||
fig.savefig('tmp/demo_auto_tune.png')
|
||||
fig.savefig("tmp/demo_auto_tune.png")
|
||||
|
||||
|
||||
print("[%.3f s] final result:" % (time.time() - t0))
|
||||
|
||||
@@ -17,13 +17,13 @@ from faiss.contrib.client_server import run_index_server, ClientIndex
|
||||
|
||||
|
||||
def ivecs_read(fname):
|
||||
a = np.fromfile(fname, dtype='int32')
|
||||
a = np.fromfile(fname, dtype="int32")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:].copy()
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -32,7 +32,7 @@ def fvecs_read(fname):
|
||||
|
||||
stage = int(sys.argv[1])
|
||||
|
||||
tmpdir = '/tmp/'
|
||||
tmpdir = "/tmp/"
|
||||
|
||||
if stage == 0:
|
||||
# train the index
|
||||
@@ -57,10 +57,10 @@ if 1 <= stage <= 4:
|
||||
|
||||
|
||||
machine_ports = [
|
||||
('localhost', 12010),
|
||||
('localhost', 12011),
|
||||
('localhost', 12012),
|
||||
('localhost', 12013),
|
||||
("localhost", 12010),
|
||||
("localhost", 12011),
|
||||
("localhost", 12012),
|
||||
("localhost", 12013),
|
||||
]
|
||||
v6 = False
|
||||
|
||||
@@ -78,7 +78,7 @@ if 5 <= stage <= 8:
|
||||
|
||||
if stage == 9:
|
||||
client_index = ClientIndex(machine_ports)
|
||||
print('index size:', client_index.ntotal)
|
||||
print("index size:", client_index.ntotal)
|
||||
client_index.set_nprobe(16)
|
||||
|
||||
# load query vectors and ground-truth
|
||||
|
||||
@@ -33,18 +33,19 @@ class DatasetAssignDistributedGPU(clustering.DatasetAssign):
|
||||
sizes = torch.zeros(nproc, device=self.device, dtype=torch.int64)
|
||||
sizes[rank] = n
|
||||
torch.distributed.all_gather(
|
||||
[sizes[i:i + 1] for i in range(nproc)], sizes[rank:rank + 1])
|
||||
[sizes[i : i + 1] for i in range(nproc)], sizes[rank : rank + 1]
|
||||
)
|
||||
self.sizes = sizes.cpu().numpy()
|
||||
|
||||
# begin & end of each shard
|
||||
self.cs = np.zeros(nproc + 1, dtype='int64')
|
||||
self.cs = np.zeros(nproc + 1, dtype="int64")
|
||||
self.cs[1:] = np.cumsum(self.sizes)
|
||||
|
||||
def count(self):
|
||||
return int(self.sizes.sum())
|
||||
|
||||
def int_to_slaves(self, i):
|
||||
" broadcast an int to all workers "
|
||||
"broadcast an int to all workers"
|
||||
rank = self.rank
|
||||
tab = torch.zeros(1, device=self.device, dtype=torch.int64)
|
||||
if rank == 0:
|
||||
@@ -64,7 +65,8 @@ class DatasetAssignDistributedGPU(clustering.DatasetAssign):
|
||||
indices = torch.from_numpy(indices).to(self.device)
|
||||
else:
|
||||
indices = torch.zeros(
|
||||
len_indices, dtype=torch.int64, device=self.device)
|
||||
len_indices, dtype=torch.int64, device=self.device
|
||||
)
|
||||
torch.distributed.broadcast(indices, 0)
|
||||
|
||||
# select subset of indices
|
||||
@@ -73,8 +75,8 @@ class DatasetAssignDistributedGPU(clustering.DatasetAssign):
|
||||
|
||||
mask = torch.logical_and(indices < i1, indices >= i0)
|
||||
output = torch.zeros(
|
||||
len_indices, self.x.shape[1],
|
||||
dtype=self.x.dtype, device=self.device)
|
||||
len_indices, self.x.shape[1], dtype=self.x.dtype, device=self.device
|
||||
)
|
||||
output[mask] = self.x[indices[mask] - i0]
|
||||
torch.distributed.reduce(output, 0) # sum
|
||||
if rank == 0:
|
||||
@@ -94,12 +96,14 @@ class DatasetAssignDistributedGPU(clustering.DatasetAssign):
|
||||
|
||||
if rank != 0:
|
||||
centroids = torch.zeros(
|
||||
nc, self.x.shape[1], dtype=self.x.dtype, device=self.device)
|
||||
nc, self.x.shape[1], dtype=self.x.dtype, device=self.device
|
||||
)
|
||||
torch.distributed.broadcast(centroids, 0)
|
||||
|
||||
# perform search
|
||||
D, I = faiss.knn_gpu(
|
||||
self.res, self.x, centroids, 1, device=self.device.index)
|
||||
self.res, self.x, centroids, 1, device=self.device.index
|
||||
)
|
||||
|
||||
I = I.ravel()
|
||||
D = D.ravel()
|
||||
@@ -120,11 +124,13 @@ class DatasetAssignDistributedGPU(clustering.DatasetAssign):
|
||||
all_I = torch.zeros(self.count(), dtype=I.dtype, device=device)
|
||||
all_D = torch.zeros(self.count(), dtype=D.dtype, device=device)
|
||||
torch.distributed.gather(
|
||||
I, [all_I[self.cs[r]:self.cs[r + 1]] for r in range(nproc)],
|
||||
I,
|
||||
[all_I[self.cs[r] : self.cs[r + 1]] for r in range(nproc)],
|
||||
dst=0,
|
||||
)
|
||||
torch.distributed.gather(
|
||||
D, [all_D[self.cs[r]:self.cs[r + 1]] for r in range(nproc)],
|
||||
D,
|
||||
[all_D[self.cs[r] : self.cs[r + 1]] for r in range(nproc)],
|
||||
dst=0,
|
||||
)
|
||||
return all_I.cpu().numpy(), all_D, sum_per_centroid
|
||||
@@ -160,7 +166,8 @@ if __name__ == "__main__":
|
||||
if rank == 0:
|
||||
print(f"sizes = {da.sizes}")
|
||||
centroids, iteration_stats = clustering.kmeans(
|
||||
k, da, niter=niter, return_stats=True)
|
||||
k, da, niter=niter, return_stats=True
|
||||
)
|
||||
print("clusters:", centroids.cpu().numpy())
|
||||
else:
|
||||
# make sure the iterations are aligned with master
|
||||
|
||||
@@ -15,13 +15,13 @@ from faiss.contrib.ondisk import merge_ondisk
|
||||
|
||||
|
||||
def ivecs_read(fname):
|
||||
a = np.fromfile(fname, dtype='int32')
|
||||
a = np.fromfile(fname, dtype="int32")
|
||||
d = a[0]
|
||||
return a.reshape(-1, d + 1)[:, 1:].copy()
|
||||
|
||||
|
||||
def fvecs_read(fname):
|
||||
return ivecs_read(fname).view('float32')
|
||||
return ivecs_read(fname).view("float32")
|
||||
|
||||
|
||||
#################################################################
|
||||
@@ -30,7 +30,7 @@ def fvecs_read(fname):
|
||||
|
||||
stage = int(sys.argv[1])
|
||||
|
||||
tmpdir = '/tmp/'
|
||||
tmpdir = "/tmp/"
|
||||
|
||||
if stage == 0:
|
||||
# train the index
|
||||
@@ -55,14 +55,11 @@ if 1 <= stage <= 4:
|
||||
|
||||
if stage == 5:
|
||||
|
||||
print('loading trained index')
|
||||
print("loading trained index")
|
||||
# construct the output index
|
||||
index = faiss.read_index(tmpdir + "trained.index")
|
||||
|
||||
block_fnames = [
|
||||
tmpdir + "block_%d.index" % bno
|
||||
for bno in range(4)
|
||||
]
|
||||
block_fnames = [tmpdir + "block_%d.index" % bno for bno in range(4)]
|
||||
|
||||
merge_ondisk(index, block_fnames, tmpdir + "merged_index.ivfdata")
|
||||
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
"""
|
||||
This demonstrates how to reproduce the QINCo paper results using the Faiss
|
||||
QINCo implementation. The code loads the reference model because training
|
||||
QINCo implementation. The code loads the reference model because training
|
||||
is not implemented in Faiss.
|
||||
|
||||
Prepare the data with
|
||||
@@ -41,7 +41,7 @@ with torch.no_grad():
|
||||
torch.set_num_threads(1)
|
||||
faiss.omp_set_num_threads(1)
|
||||
|
||||
x_base = bvecs_mmap("/tmp/bigann1M.bvecs")[:1000].astype('float32')
|
||||
x_base = bvecs_mmap("/tmp/bigann1M.bvecs")[:1000].astype("float32")
|
||||
x_scaled = torch.from_numpy(x_base) / qinco.db_scale
|
||||
|
||||
t0 = time.time()
|
||||
|
||||
@@ -29,7 +29,7 @@ def main(args: argparse.Namespace):
|
||||
), "num of embeddings per file should divide total num of embeddings"
|
||||
for i in range(num_batches):
|
||||
xb_batch = ssnpp_data[
|
||||
i * args.data_batch:(i + 1) * args.data_batch, :
|
||||
i * args.data_batch : (i + 1) * args.data_batch, :
|
||||
]
|
||||
filename = args.output_dir + f"/ssnpp_{(i):010}.npy"
|
||||
np.save(filename, xb_batch)
|
||||
|
||||
@@ -111,7 +111,7 @@ class MultiFileVectorDataset:
|
||||
xb = xb[start:]
|
||||
start = 0
|
||||
req = min(batch_size - rem, xb.shape[0])
|
||||
buffer[rem:rem + req] = xb[:req]
|
||||
buffer[rem : rem + req] = xb[:req]
|
||||
rem += req
|
||||
if rem == batch_size:
|
||||
if self.normalize:
|
||||
|
||||
@@ -179,7 +179,9 @@ class OfflineIVF:
|
||||
idxs.append(np.empty((0,), dtype=np.uint32))
|
||||
bs = 1_000_000
|
||||
i = 0
|
||||
for buffer in tqdm(self._iterate_transformed(self.xb_ds, 0, bs, np.float32)):
|
||||
for buffer in tqdm(
|
||||
self._iterate_transformed(self.xb_ds, 0, bs, np.float32)
|
||||
):
|
||||
for j in range(len(codecs)):
|
||||
codec, codeset, idx = codecs[j], codesets[j], idxs[j]
|
||||
uniq = codeset.insert(codec.sa_encode(buffer))
|
||||
@@ -272,13 +274,17 @@ class OfflineIVF:
|
||||
assert xb_j.shape[1] == index.chain.at(0).d_out
|
||||
index_ivf.add_with_ids(
|
||||
xb_j,
|
||||
np.arange(start + jj, start + jj + xb_j.shape[0]),
|
||||
np.arange(
|
||||
start + jj, start + jj + xb_j.shape[0]
|
||||
),
|
||||
)
|
||||
else:
|
||||
assert xb_j.shape[1] == index.d
|
||||
index.add_with_ids(
|
||||
xb_j,
|
||||
np.arange(start + jj, start + jj + xb_j.shape[0]),
|
||||
np.arange(
|
||||
start + jj, start + jj + xb_j.shape[0]
|
||||
),
|
||||
)
|
||||
jj += xb_j.shape[0]
|
||||
logging.info(jj)
|
||||
@@ -413,7 +419,9 @@ class OfflineIVF:
|
||||
D_a_ann_file = (
|
||||
f"{self.eval_dir}/D_a_ann_{index_factory_fn}_np{nprobe}.npy"
|
||||
)
|
||||
D_a_ann_refined_file = f"{self.eval_dir}/D_a_ann_refined_{index_factory_fn}_np{nprobe}.npy"
|
||||
D_a_ann_refined_file = (
|
||||
f"{self.eval_dir}/D_a_ann_refined_{index_factory_fn}_np{nprobe}.npy"
|
||||
)
|
||||
D_b_gt_file = f"{self.eval_dir}/D_b_gt.npy"
|
||||
D_b_ann_file = (
|
||||
f"{self.eval_dir}/D_b_ann_{index_factory_fn}_np{nprobe}.npy"
|
||||
@@ -684,26 +692,22 @@ class OfflineIVF:
|
||||
else:
|
||||
d = self.input_d
|
||||
with open(Ifn, "xb") as f, open(Dfn, "xb") as g:
|
||||
xq_i = np.empty(
|
||||
shape=(self.xq_bs, d), dtype=np.float16
|
||||
)
|
||||
xq_i = np.empty(shape=(self.xq_bs, d), dtype=np.float16)
|
||||
q_assign = np.empty(
|
||||
(self.xq_bs, self.nprobe), dtype=np.int32
|
||||
)
|
||||
j = 0
|
||||
quantizer = faiss.index_cpu_to_all_gpus(
|
||||
index_ivf.quantizer
|
||||
)
|
||||
quantizer = faiss.index_cpu_to_all_gpus(index_ivf.quantizer)
|
||||
for xq_i_j in tqdm(
|
||||
self._iterate_transformed(
|
||||
self.xq_ds, i, min(100_000, self.xq_bs), np.float16
|
||||
),
|
||||
file=sys.stdout,
|
||||
):
|
||||
xq_i[j:j + xq_i_j.shape[0]] = xq_i_j
|
||||
xq_i[j : j + xq_i_j.shape[0]] = xq_i_j
|
||||
(
|
||||
_,
|
||||
q_assign[j:j + xq_i_j.shape[0]],
|
||||
q_assign[j : j + xq_i_j.shape[0]],
|
||||
) = quantizer.search(xq_i_j, self.nprobe)
|
||||
j += xq_i_j.shape[0]
|
||||
assert j <= xq_i.shape[0]
|
||||
@@ -847,7 +851,8 @@ class OfflineIVF:
|
||||
for j in range(SMALL_DATA_SAMPLE):
|
||||
assert np.where(I[j] == j + r)[0].size > 0, (
|
||||
f"I[j]: {I[j]}, j: {j}, i: {i}, shard_size:"
|
||||
f" {self.shard_size}")
|
||||
f" {self.shard_size}"
|
||||
)
|
||||
|
||||
logging.info("search results...")
|
||||
index_ivf.nprobe = self.nprobe
|
||||
|
||||
@@ -38,9 +38,9 @@ class TestUtilsMethods(unittest.TestCase):
|
||||
data_creator.create_test_data()
|
||||
args = data_creator.setup_cli()
|
||||
cfg = load_config(args.config)
|
||||
db_iterator = create_dataset_from_oivf_config(
|
||||
cfg, args.xb
|
||||
).iterate(0, TEST_BATCH_SIZE, np.float32)
|
||||
db_iterator = create_dataset_from_oivf_config(cfg, args.xb).iterate(
|
||||
0, TEST_BATCH_SIZE, np.float32
|
||||
)
|
||||
|
||||
for i in range(len(SMALL_FILE_SIZES) - 1):
|
||||
vecs = next(db_iterator)
|
||||
@@ -65,9 +65,9 @@ class TestUtilsMethods(unittest.TestCase):
|
||||
data_creator.create_test_data()
|
||||
args = data_creator.setup_cli()
|
||||
cfg = load_config(args.config)
|
||||
db_iterator = create_dataset_from_oivf_config(
|
||||
cfg, args.xb
|
||||
).iterate(0, TEST_BATCH_SIZE, np.float32)
|
||||
db_iterator = create_dataset_from_oivf_config(cfg, args.xb).iterate(
|
||||
0, TEST_BATCH_SIZE, np.float32
|
||||
)
|
||||
|
||||
for i in range(len(LARGE_FILE_SIZES) - 1):
|
||||
vecs = next(db_iterator)
|
||||
|
||||
@@ -11,15 +11,16 @@ import numpy as np
|
||||
|
||||
def test_slow():
|
||||
d = 256
|
||||
index = faiss.index_cpu_to_gpu(faiss.StandardGpuResources(),
|
||||
0, faiss.IndexFlatL2(d))
|
||||
x = np.random.rand(10 ** 6, d).astype('float32')
|
||||
print('add')
|
||||
index = faiss.index_cpu_to_gpu(
|
||||
faiss.StandardGpuResources(), 0, faiss.IndexFlatL2(d)
|
||||
)
|
||||
x = np.random.rand(10**6, d).astype("float32")
|
||||
print("add")
|
||||
index.add(x)
|
||||
print('search')
|
||||
print("search")
|
||||
index.search(x, 10)
|
||||
print('done')
|
||||
print("done")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
test_slow()
|
||||
|
||||
@@ -28,8 +28,8 @@ from faiss.contrib import evaluation
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
"CUVS" not in faiss.get_compile_options(),
|
||||
"only if cuVS is compiled in")
|
||||
"CUVS" not in faiss.get_compile_options(), "only if cuVS is compiled in"
|
||||
)
|
||||
class TestInterop(unittest.TestCase):
|
||||
|
||||
def do_interop(self):
|
||||
@@ -40,10 +40,12 @@ class TestInterop(unittest.TestCase):
|
||||
|
||||
index = faiss.GpuIndexBinaryCagra(res, d)
|
||||
xb = np.random.randint(
|
||||
low=0, high=256, size=(1000000, d // 8), dtype=np.uint8)
|
||||
low=0, high=256, size=(1000000, d // 8), dtype=np.uint8
|
||||
)
|
||||
index.train(xb)
|
||||
queries = np.random.randint(
|
||||
low=0, high=256, size=(1000, d // 8), dtype=np.uint8)
|
||||
low=0, high=256, size=(1000, d // 8), dtype=np.uint8
|
||||
)
|
||||
Dnew, Inew = index.search(queries, k)
|
||||
|
||||
cpu_index = faiss.index_binary_gpu_to_cpu(index)
|
||||
@@ -52,7 +54,8 @@ class TestInterop(unittest.TestCase):
|
||||
evaluation.check_ref_knn_with_draws(Dref, Iref, Dnew, Inew, k)
|
||||
|
||||
deserialized_index = faiss.deserialize_index_binary(
|
||||
faiss.serialize_index_binary(cpu_index))
|
||||
faiss.serialize_index_binary(cpu_index)
|
||||
)
|
||||
|
||||
gpu_index = faiss.index_binary_cpu_to_gpu(res, 0, deserialized_index)
|
||||
Dnew2, Inew2 = gpu_index.search(queries, k)
|
||||
@@ -64,8 +67,8 @@ class TestInterop(unittest.TestCase):
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
"CUVS" not in faiss.get_compile_options(),
|
||||
"only if cuVS is compiled in")
|
||||
"CUVS" not in faiss.get_compile_options(), "only if cuVS is compiled in"
|
||||
)
|
||||
class TestComputeGT(unittest.TestCase):
|
||||
|
||||
def do_compute_GT(self, build_algo=None):
|
||||
@@ -73,10 +76,12 @@ class TestComputeGT(unittest.TestCase):
|
||||
k = 12
|
||||
flat_index = faiss.IndexBinaryFlat(d)
|
||||
xb = np.random.randint(
|
||||
low=0, high=256, size=(1000000, d // 8), dtype=np.uint8)
|
||||
low=0, high=256, size=(1000000, d // 8), dtype=np.uint8
|
||||
)
|
||||
flat_index.add(xb)
|
||||
queries = np.random.randint(
|
||||
low=0, high=256, size=(1000, d // 8), dtype=np.uint8)
|
||||
low=0, high=256, size=(1000, d // 8), dtype=np.uint8
|
||||
)
|
||||
Dref, Iref = flat_index.search(queries, k)
|
||||
|
||||
res = faiss.StandardGpuResources()
|
||||
@@ -102,8 +107,8 @@ class TestComputeGT(unittest.TestCase):
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
"CUVS" not in faiss.get_compile_options(),
|
||||
"only if cuVS is compiled in")
|
||||
"CUVS" not in faiss.get_compile_options(), "only if cuVS is compiled in"
|
||||
)
|
||||
class TestIndexBinaryIDMap(unittest.TestCase):
|
||||
"""Test IndexBinaryIDMap functionality with GpuIndexBinaryCagra"""
|
||||
|
||||
@@ -123,14 +128,16 @@ class TestIndexBinaryIDMap(unittest.TestCase):
|
||||
index_idmap = faiss.IndexBinaryIDMap(index_gpu)
|
||||
|
||||
xb = np.random.randint(
|
||||
low=0, high=256, size=(n, d // 8), dtype=np.uint8)
|
||||
low=0, high=256, size=(n, d // 8), dtype=np.uint8
|
||||
)
|
||||
ids = np.arange(1000000, 1000000 + n).astype(np.int64)
|
||||
|
||||
index_idmap.add_with_ids(xb, ids)
|
||||
|
||||
nq = 1000
|
||||
xq = np.random.randint(
|
||||
low=0, high=256, size=(nq, d // 8), dtype=np.uint8)
|
||||
low=0, high=256, size=(nq, d // 8), dtype=np.uint8
|
||||
)
|
||||
D, I = index_idmap.search(xq, k)
|
||||
|
||||
self.assertTrue(np.all(I >= 1000000))
|
||||
|
||||
@@ -21,8 +21,12 @@ class TestComputeGT(unittest.TestCase):
|
||||
k = 12
|
||||
|
||||
if numeric_type == faiss.Int8:
|
||||
data_base_nt = np.random.randint(-128, 128, size=(10000, d), dtype=np.int8)
|
||||
data_query_nt = np.random.randint(-128, 128, size=(100, d), dtype=np.int8)
|
||||
data_base_nt = np.random.randint(
|
||||
-128, 128, size=(10000, d), dtype=np.int8
|
||||
)
|
||||
data_query_nt = np.random.randint(
|
||||
-128, 128, size=(100, d), dtype=np.int8
|
||||
)
|
||||
data_base = data_base_nt.astype(np.float32)
|
||||
data_query = data_query_nt.astype(np.float32)
|
||||
else:
|
||||
@@ -32,7 +36,9 @@ class TestComputeGT(unittest.TestCase):
|
||||
# Normalize for inner product to avoid duplicate neighbors
|
||||
if metric == faiss.METRIC_INNER_PRODUCT:
|
||||
# Normalize database vectors
|
||||
data_base = data_base / np.linalg.norm(data_base, axis=1, keepdims=True)
|
||||
data_base = data_base / np.linalg.norm(
|
||||
data_base, axis=1, keepdims=True
|
||||
)
|
||||
# Normalize query vectors
|
||||
data_query = data_query / np.linalg.norm(
|
||||
data_query, axis=1, keepdims=True
|
||||
@@ -95,8 +101,12 @@ class TestInterop(unittest.TestCase):
|
||||
d = 64
|
||||
k = 12
|
||||
if numeric_type == faiss.Int8:
|
||||
data_base_nt = np.random.randint(-128, 128, size=(10000, d), dtype=np.int8)
|
||||
data_query_nt = np.random.randint(-128, 128, size=(100, d), dtype=np.int8)
|
||||
data_base_nt = np.random.randint(
|
||||
-128, 128, size=(10000, d), dtype=np.int8
|
||||
)
|
||||
data_query_nt = np.random.randint(
|
||||
-128, 128, size=(100, d), dtype=np.int8
|
||||
)
|
||||
data_base = data_base_nt.astype(np.float32)
|
||||
data_query = data_query_nt.astype(np.float32)
|
||||
else:
|
||||
@@ -122,10 +132,14 @@ class TestInterop(unittest.TestCase):
|
||||
|
||||
evaluation.check_ref_knn_with_draws(Dref, Iref, Dnew, Inew, k)
|
||||
|
||||
deserialized_index = faiss.deserialize_index(faiss.serialize_index(cpu_index))
|
||||
deserialized_index = faiss.deserialize_index(
|
||||
faiss.serialize_index(cpu_index)
|
||||
)
|
||||
|
||||
gpu_index = faiss.index_cpu_to_gpu(res, 0, deserialized_index)
|
||||
Dnew2, Inew2 = gpu_index.search(data_query_nt, k, numeric_type=numeric_type)
|
||||
Dnew2, Inew2 = gpu_index.search(
|
||||
data_query_nt, k, numeric_type=numeric_type
|
||||
)
|
||||
|
||||
evaluation.check_ref_knn_with_draws(Dnew2, Inew2, Dnew, Inew, k)
|
||||
|
||||
@@ -178,8 +192,12 @@ class TestIDMapCagra(unittest.TestCase):
|
||||
d = 64
|
||||
k = 12
|
||||
if numeric_type == faiss.Int8:
|
||||
data_base_nt = np.random.randint(-128, 128, size=(10000, d), dtype=np.int8)
|
||||
data_query_nt = np.random.randint(-128, 128, size=(100, d), dtype=np.int8)
|
||||
data_base_nt = np.random.randint(
|
||||
-128, 128, size=(10000, d), dtype=np.int8
|
||||
)
|
||||
data_query_nt = np.random.randint(
|
||||
-128, 128, size=(100, d), dtype=np.int8
|
||||
)
|
||||
data_base = data_base_nt.astype(np.float32)
|
||||
data_query = data_query_nt.astype(np.float32)
|
||||
else:
|
||||
@@ -202,7 +220,9 @@ class TestIDMapCagra(unittest.TestCase):
|
||||
idMapIndex.train(data_base_nt, numeric_type=numeric_type)
|
||||
ids = np.array([i for i in range(10000)])
|
||||
idMapIndex.add_with_ids(data_base_nt, ids, numeric_type=numeric_type)
|
||||
Dnew, Inew = idMapIndex.search(data_query_nt, k, numeric_type=numeric_type)
|
||||
Dnew, Inew = idMapIndex.search(
|
||||
data_query_nt, k, numeric_type=numeric_type
|
||||
)
|
||||
|
||||
evaluation.check_ref_knn_with_draws(Dref, Iref, Dnew, Inew, k)
|
||||
|
||||
|
||||
@@ -11,9 +11,13 @@ import numpy as np
|
||||
from common_faiss_tests import get_dataset_2
|
||||
|
||||
from faiss.contrib import datasets, evaluation, big_batch_search
|
||||
from faiss.contrib.exhaustive_search import knn_ground_truth, \
|
||||
range_ground_truth, range_search_gpu, \
|
||||
range_search_max_results, exponential_query_iterator
|
||||
from faiss.contrib.exhaustive_search import (
|
||||
knn_ground_truth,
|
||||
range_ground_truth,
|
||||
range_search_gpu,
|
||||
range_search_max_results,
|
||||
exponential_query_iterator,
|
||||
)
|
||||
|
||||
|
||||
class TestComputeGT(unittest.TestCase):
|
||||
@@ -30,7 +34,7 @@ class TestComputeGT(unittest.TestCase):
|
||||
|
||||
def matrix_iterator(xb, bs):
|
||||
for i0 in range(0, xb.shape[0], bs):
|
||||
yield xb[i0:i0 + bs]
|
||||
yield xb[i0 : i0 + bs]
|
||||
|
||||
Dnew, Inew = knn_ground_truth(xq, matrix_iterator(xb, 1000), 10)
|
||||
|
||||
@@ -49,12 +53,11 @@ class TestComputeGT(unittest.TestCase):
|
||||
ref_lims, ref_D, ref_I = index.range_search(xq, threshold)
|
||||
|
||||
new_lims, new_D, new_I = range_ground_truth(
|
||||
xq, ds.database_iterator(bs=100), threshold,
|
||||
metric_type=metric)
|
||||
xq, ds.database_iterator(bs=100), threshold, metric_type=metric
|
||||
)
|
||||
|
||||
evaluation.check_ref_range_results(
|
||||
ref_lims, ref_D, ref_I,
|
||||
new_lims, new_D, new_I
|
||||
ref_lims, ref_D, ref_I, new_lims, new_D, new_I
|
||||
)
|
||||
|
||||
def test_range_L2(self):
|
||||
@@ -84,14 +87,16 @@ class TestComputeGT(unittest.TestCase):
|
||||
query_iterator = exponential_query_iterator(xq)
|
||||
|
||||
radius1, lims_new, Dnew, Inew = range_search_max_results(
|
||||
index, query_iterator, ds.d // 2,
|
||||
min_results=Dref.size, clip_to_min=True,
|
||||
ngpu=1
|
||||
index,
|
||||
query_iterator,
|
||||
ds.d // 2,
|
||||
min_results=Dref.size,
|
||||
clip_to_min=True,
|
||||
ngpu=1,
|
||||
)
|
||||
|
||||
evaluation.check_ref_range_results(
|
||||
lims_ref, Dref, Iref,
|
||||
lims_new, Dnew, Inew
|
||||
lims_ref, Dref, Iref, lims_new, Dnew, Inew
|
||||
)
|
||||
|
||||
@unittest.skipIf(faiss.get_num_gpus() < 2, "multiple GPU only test")
|
||||
@@ -113,17 +118,20 @@ class TestBigBatchSearch(unittest.TestCase):
|
||||
|
||||
def pairwise_distances(xq, xb, metric=faiss.METRIC_L2):
|
||||
return faiss.pairwise_distance_gpu(
|
||||
res, xq, xb, metric=faiss.METRIC_L2)
|
||||
res, xq, xb, metric=faiss.METRIC_L2
|
||||
)
|
||||
|
||||
def knn_function(xq, xb, k, metric=faiss.METRIC_L2):
|
||||
return faiss.knn_gpu(res, xq, xb, k, metric=faiss.METRIC_L2)
|
||||
|
||||
for method in "pairwise_distances", "knn_function":
|
||||
Dnew, Inew = big_batch_search.big_batch_search(
|
||||
index, ds.get_queries(),
|
||||
k, method=method,
|
||||
index,
|
||||
ds.get_queries(),
|
||||
k,
|
||||
method=method,
|
||||
pairwise_distances=pairwise_distances,
|
||||
knn=knn_function
|
||||
knn=knn_function,
|
||||
)
|
||||
self.assertLess((Inew != Iref).sum() / Iref.size, 1e-4)
|
||||
np.testing.assert_almost_equal(Dnew, Dref, decimal=4)
|
||||
@@ -151,16 +159,22 @@ class TestBigBatchSearchMultiGPU(unittest.TestCase):
|
||||
|
||||
def knn_function(xq, xb, k, metric=faiss.METRIC_L2, thread_id=None):
|
||||
return faiss.knn_gpu(
|
||||
res[thread_id], xq, xb, k,
|
||||
metric=faiss.METRIC_L2, device=thread_id
|
||||
res[thread_id],
|
||||
xq,
|
||||
xb,
|
||||
k,
|
||||
metric=faiss.METRIC_L2,
|
||||
device=thread_id,
|
||||
)
|
||||
|
||||
Dnew, Inew = big_batch_search.big_batch_search(
|
||||
index, ds.get_queries(),
|
||||
k, method="knn_function",
|
||||
index,
|
||||
ds.get_queries(),
|
||||
k,
|
||||
method="knn_function",
|
||||
knn=knn_function,
|
||||
threaded=8,
|
||||
computation_threads=ngpu
|
||||
computation_threads=ngpu,
|
||||
)
|
||||
self.assertLess((Inew != Iref).sum() / Iref.size, 1e-4)
|
||||
np.testing.assert_almost_equal(Dnew, Dref, decimal=4)
|
||||
@@ -192,18 +206,17 @@ class TestRangeSearchGpu(unittest.TestCase):
|
||||
|
||||
# mixed GPU / CPU run
|
||||
Lnew, Dnew, Inew = range_search_gpu(
|
||||
ds.get_queries(), threshold, index_gpu, index_cpu, gpu_k=4)
|
||||
evaluation.check_ref_range_results(
|
||||
Lref, Dref, Iref,
|
||||
Lnew, Dnew, Inew
|
||||
ds.get_queries(), threshold, index_gpu, index_cpu, gpu_k=4
|
||||
)
|
||||
evaluation.check_ref_range_results(Lref, Dref, Iref, Lnew, Dnew, Inew)
|
||||
|
||||
# also test the version without CPU search
|
||||
Lnew2, Dnew2, Inew2 = range_search_gpu(
|
||||
ds.get_queries(), threshold, index_gpu, None, gpu_k=4)
|
||||
ds.get_queries(), threshold, index_gpu, None, gpu_k=4
|
||||
)
|
||||
for q in range(ds.nq):
|
||||
ref = Iref[Lref[q]:Lref[q+1]]
|
||||
new = Inew2[Lnew2[q]:Lnew2[q+1]]
|
||||
ref = Iref[Lref[q] : Lref[q + 1]]
|
||||
new = Inew2[Lnew2[q] : Lnew2[q + 1]]
|
||||
if nres_per_query[q] <= 4:
|
||||
self.assertEqual(set(ref), set(new))
|
||||
else:
|
||||
|
||||
@@ -11,8 +11,8 @@ from faiss.contrib.datasets import SyntheticDataset
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
"CUVS" not in faiss.get_compile_options(),
|
||||
"only if CUVS is compiled in")
|
||||
"CUVS" not in faiss.get_compile_options(), "only if CUVS is compiled in"
|
||||
)
|
||||
class TestBfKnn(unittest.TestCase):
|
||||
|
||||
def test_large_k_search(self):
|
||||
@@ -29,7 +29,6 @@ class TestBfKnn(unittest.TestCase):
|
||||
_, I = index_gpu.search(ds.get_queries(), k)
|
||||
np.testing.assert_equal(I.shape, (ds.nq, k))
|
||||
|
||||
|
||||
def test_bfKnn(self):
|
||||
|
||||
ds = SyntheticDataset(32, 0, 4321, 1234)
|
||||
@@ -40,13 +39,15 @@ class TestBfKnn(unittest.TestCase):
|
||||
|
||||
# Faiss internal implementation
|
||||
Dnew, Inew = faiss.knn_gpu(
|
||||
res, ds.get_queries(), ds.get_database(), 12, use_cuvs=False)
|
||||
res, ds.get_queries(), ds.get_database(), 12, use_cuvs=False
|
||||
)
|
||||
np.testing.assert_allclose(Dref, Dnew, atol=1e-4)
|
||||
np.testing.assert_array_equal(Iref, Inew)
|
||||
|
||||
# cuVS version
|
||||
Dnew, Inew = faiss.knn_gpu(
|
||||
res, ds.get_queries(), ds.get_database(), 12, use_cuvs=True)
|
||||
res, ds.get_queries(), ds.get_database(), 12, use_cuvs=True
|
||||
)
|
||||
np.testing.assert_allclose(Dref, Dnew, atol=1e-4)
|
||||
np.testing.assert_array_equal(Iref, Inew)
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
from __future__ import (
|
||||
absolute_import,
|
||||
division,
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
@@ -15,12 +20,12 @@ from common_faiss_tests import get_dataset_2
|
||||
class ReferencedObject(unittest.TestCase):
|
||||
|
||||
d = 16
|
||||
xb = np.random.rand(256, d).astype('float32')
|
||||
xb = np.random.rand(256, d).astype("float32")
|
||||
nlist = 128
|
||||
|
||||
d_bin = 256
|
||||
xb_bin = np.random.randint(256, size=(10000, d_bin // 8)).astype('uint8')
|
||||
xq_bin = np.random.randint(256, size=(1000, d_bin // 8)).astype('uint8')
|
||||
xb_bin = np.random.randint(256, size=(10000, d_bin // 8)).astype("uint8")
|
||||
xq_bin = np.random.randint(256, size=(1000, d_bin // 8)).astype("uint8")
|
||||
|
||||
def test_proxy(self):
|
||||
index = faiss.IndexReplicas()
|
||||
@@ -33,32 +38,33 @@ class ReferencedObject(unittest.TestCase):
|
||||
|
||||
def test_resources(self):
|
||||
# this used to crash!
|
||||
index = faiss.index_cpu_to_gpu(faiss.StandardGpuResources(), 0,
|
||||
faiss.IndexFlatL2(self.d))
|
||||
index = faiss.index_cpu_to_gpu(
|
||||
faiss.StandardGpuResources(), 0, faiss.IndexFlatL2(self.d)
|
||||
)
|
||||
index.add(self.xb)
|
||||
|
||||
def test_flat(self):
|
||||
index = faiss.GpuIndexFlat(faiss.StandardGpuResources(),
|
||||
self.d, faiss.METRIC_L2)
|
||||
index = faiss.GpuIndexFlat(
|
||||
faiss.StandardGpuResources(), self.d, faiss.METRIC_L2
|
||||
)
|
||||
index.add(self.xb)
|
||||
|
||||
def test_ivfflat(self):
|
||||
index = faiss.GpuIndexIVFFlat(
|
||||
faiss.StandardGpuResources(),
|
||||
self.d, self.nlist, faiss.METRIC_L2)
|
||||
faiss.StandardGpuResources(), self.d, self.nlist, faiss.METRIC_L2
|
||||
)
|
||||
index.train(self.xb)
|
||||
|
||||
def test_ivfpq(self):
|
||||
index_cpu = faiss.IndexIVFPQ(
|
||||
faiss.IndexFlatL2(self.d),
|
||||
self.d, self.nlist, 2, 8)
|
||||
faiss.IndexFlatL2(self.d), self.d, self.nlist, 2, 8
|
||||
)
|
||||
# speed up test
|
||||
index_cpu.pq.cp.niter = 2
|
||||
index_cpu.do_polysemous_training = False
|
||||
index_cpu.train(self.xb)
|
||||
|
||||
index = faiss.GpuIndexIVFPQ(
|
||||
faiss.StandardGpuResources(), index_cpu)
|
||||
index = faiss.GpuIndexIVFPQ(faiss.StandardGpuResources(), index_cpu)
|
||||
index.add(self.xb)
|
||||
|
||||
def test_binary_flat(self):
|
||||
@@ -68,8 +74,9 @@ class ReferencedObject(unittest.TestCase):
|
||||
index_ref.add(self.xb_bin)
|
||||
D_ref, I_ref = index_ref.search(self.xq_bin, k)
|
||||
|
||||
index = faiss.GpuIndexBinaryFlat(faiss.StandardGpuResources(),
|
||||
self.d_bin)
|
||||
index = faiss.GpuIndexBinaryFlat(
|
||||
faiss.StandardGpuResources(), self.d_bin
|
||||
)
|
||||
index.add(self.xb_bin)
|
||||
D, I = index.search(self.xq_bin, k)
|
||||
|
||||
@@ -89,15 +96,17 @@ class ReferencedObject(unittest.TestCase):
|
||||
|
||||
def test_stress(self):
|
||||
# a mixture of the above, from issue #631
|
||||
target = np.random.rand(50, 16).astype('float32')
|
||||
target = np.random.rand(50, 16).astype("float32")
|
||||
|
||||
index = faiss.IndexReplicas()
|
||||
size, dim = target.shape
|
||||
num_gpu = 4
|
||||
for _i in range(num_gpu):
|
||||
config = faiss.GpuIndexFlatConfig()
|
||||
config.device = 0 # simulate on a single GPU
|
||||
sub_index = faiss.GpuIndexFlatIP(faiss.StandardGpuResources(), dim, config)
|
||||
config.device = 0 # simulate on a single GPU
|
||||
sub_index = faiss.GpuIndexFlatIP(
|
||||
faiss.StandardGpuResources(), dim, config
|
||||
)
|
||||
index.addIndex(sub_index)
|
||||
|
||||
index = faiss.IndexIDMap(index)
|
||||
@@ -105,7 +114,6 @@ class ReferencedObject(unittest.TestCase):
|
||||
index.add_with_ids(target, ids)
|
||||
|
||||
|
||||
|
||||
class TestGPUKmeans(unittest.TestCase):
|
||||
|
||||
def test_kmeans(self):
|
||||
@@ -113,7 +121,7 @@ class TestGPUKmeans(unittest.TestCase):
|
||||
nb = 1000
|
||||
k = 10
|
||||
rs = np.random.RandomState(123)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
|
||||
km1 = faiss.Kmeans(d, k)
|
||||
obj1 = km1.train(xb)
|
||||
@@ -153,8 +161,8 @@ class TestAlternativeDistances(unittest.TestCase):
|
||||
nq = 100
|
||||
|
||||
rs = np.random.RandomState(123)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
index_ref = faiss.IndexFlat(d, metric)
|
||||
index_ref.metric_arg = metric_arg
|
||||
@@ -176,8 +184,8 @@ class TestAlternativeDistances(unittest.TestCase):
|
||||
np.testing.assert_array_equal(Inew, Iref)
|
||||
|
||||
@unittest.skipIf(
|
||||
"CUVS" in faiss.get_compile_options(),
|
||||
"only if CUVS is not compiled in")
|
||||
"CUVS" in faiss.get_compile_options(), "only if CUVS is not compiled in"
|
||||
)
|
||||
def do_test_gower(self):
|
||||
"""Special test for Gower distance with mixed numeric/categorical data"""
|
||||
res = faiss.StandardGpuResources()
|
||||
@@ -188,16 +196,16 @@ class TestAlternativeDistances(unittest.TestCase):
|
||||
rs = np.random.RandomState(123)
|
||||
|
||||
# Create mixed data: first half numeric [0,1], second half categorical (negative)
|
||||
xb = np.zeros((nb, d), dtype='float32')
|
||||
xq = np.zeros((nq, d), dtype='float32')
|
||||
xb = np.zeros((nb, d), dtype="float32")
|
||||
xq = np.zeros((nq, d), dtype="float32")
|
||||
|
||||
# Numeric features in [0,1] range
|
||||
xb[:, :d//2] = rs.rand(nb, d//2)
|
||||
xq[:, :d//2] = rs.rand(nq, d//2)
|
||||
xb[:, : d // 2] = rs.rand(nb, d // 2)
|
||||
xq[:, : d // 2] = rs.rand(nq, d // 2)
|
||||
|
||||
# Categorical features (negative integers)
|
||||
xb[:, d//2:] = -rs.randint(1, 5, size=(nb, d//2)).astype('float32')
|
||||
xq[:, d//2:] = -rs.randint(1, 5, size=(nq, d//2)).astype('float32')
|
||||
xb[:, d // 2 :] = -rs.randint(1, 5, size=(nb, d // 2)).astype("float32")
|
||||
xq[:, d // 2 :] = -rs.randint(1, 5, size=(nq, d // 2)).astype("float32")
|
||||
|
||||
# CPU reference
|
||||
index_ref = faiss.IndexFlat(d, faiss.METRIC_GOWER)
|
||||
@@ -235,7 +243,9 @@ class TestGpuRef(unittest.TestCase):
|
||||
def test_gpu_ref(self):
|
||||
# this crashes
|
||||
dim = 256
|
||||
training_data = np.random.randint(256, size=(10000, dim // 8)).astype('uint8')
|
||||
training_data = np.random.randint(256, size=(10000, dim // 8)).astype(
|
||||
"uint8"
|
||||
)
|
||||
centroids = 330
|
||||
|
||||
def create_cpu(dim):
|
||||
@@ -267,7 +277,7 @@ def make_t(num, d, clamp=False, seed=None):
|
||||
|
||||
x = rs.rand(num, d).astype(np.float32)
|
||||
if clamp:
|
||||
x = (x * 255).astype('uint8').astype('float32')
|
||||
x = (x * 255).astype("uint8").astype("float32")
|
||||
return x
|
||||
|
||||
|
||||
@@ -315,10 +325,8 @@ class TestKnn(unittest.TestCase):
|
||||
|
||||
if vectorsMemoryLimit > 0 or queriesMemoryLimit > 0:
|
||||
faiss.bfKnn_tiling(
|
||||
res,
|
||||
params,
|
||||
vectorsMemoryLimit,
|
||||
queriesMemoryLimit)
|
||||
res, params, vectorsMemoryLimit, queriesMemoryLimit
|
||||
)
|
||||
else:
|
||||
faiss.bfKnn(res, params)
|
||||
|
||||
@@ -326,9 +334,16 @@ class TestKnn(unittest.TestCase):
|
||||
np.testing.assert_array_equal(out_i, ref_i)
|
||||
|
||||
faiss.knn_gpu(
|
||||
res, qs, xs, k, out_d, out_i, device=gpu_id,
|
||||
res,
|
||||
qs,
|
||||
xs,
|
||||
k,
|
||||
out_d,
|
||||
out_i,
|
||||
device=gpu_id,
|
||||
vectorsMemoryLimit=vectorsMemoryLimit,
|
||||
queriesMemoryLimit=queriesMemoryLimit)
|
||||
queriesMemoryLimit=queriesMemoryLimit,
|
||||
)
|
||||
|
||||
np.testing.assert_allclose(ref_d, out_d, atol=1e-5)
|
||||
np.testing.assert_array_equal(out_i, ref_i)
|
||||
@@ -368,8 +383,10 @@ class TestKnn(unittest.TestCase):
|
||||
|
||||
faiss.bfKnn(res, params)
|
||||
|
||||
self.assertGreaterEqual((out_i_f16 == ref_i_f16).sum(), ref_i_f16.size - 5)
|
||||
np.testing.assert_allclose(ref_d_f16, out_d_f16, atol = 2e-3)
|
||||
self.assertGreaterEqual(
|
||||
(out_i_f16 == ref_i_f16).sum(), ref_i_f16.size - 5
|
||||
)
|
||||
np.testing.assert_allclose(ref_d_f16, out_d_f16, atol=2e-3)
|
||||
|
||||
|
||||
class TestAllPairwiseDistance(unittest.TestCase):
|
||||
@@ -382,7 +399,7 @@ class TestAllPairwiseDistance(unittest.TestCase):
|
||||
faiss.METRIC_Canberra,
|
||||
faiss.METRIC_BrayCurtis,
|
||||
faiss.METRIC_JensenShannon,
|
||||
faiss.METRIC_Jaccard
|
||||
faiss.METRIC_Jaccard,
|
||||
]
|
||||
|
||||
for metric in metrics:
|
||||
@@ -409,7 +426,7 @@ class TestAllPairwiseDistance(unittest.TestCase):
|
||||
# Try f32 data/queries
|
||||
params = faiss.GpuDistanceParams()
|
||||
params.metric = metric
|
||||
params.k = -1 # all pairwise
|
||||
params.k = -1 # all pairwise
|
||||
params.dims = d
|
||||
params.vectors = faiss.swig_ptr(xs)
|
||||
params.numVectors = nb
|
||||
@@ -429,7 +446,7 @@ class TestAllPairwiseDistance(unittest.TestCase):
|
||||
if faiss.is_similarity_metric(metric):
|
||||
ref_d = np.sort(ref_d, axis=1)
|
||||
|
||||
print('f32', np.abs(ref_d - out_d).max())
|
||||
print("f32", np.abs(ref_d - out_d).max())
|
||||
|
||||
np.testing.assert_allclose(ref_d, out_d, atol=1e-5)
|
||||
|
||||
@@ -462,9 +479,9 @@ class TestAllPairwiseDistance(unittest.TestCase):
|
||||
if faiss.is_similarity_metric(metric):
|
||||
ref_d_f16 = np.sort(ref_d_f16, axis=1)
|
||||
|
||||
print('f16', np.abs(ref_d_f16 - out_d_f16).max())
|
||||
print("f16", np.abs(ref_d_f16 - out_d_f16).max())
|
||||
|
||||
np.testing.assert_allclose(ref_d_f16, out_d_f16, atol = 4e-3)
|
||||
np.testing.assert_allclose(ref_d_f16, out_d_f16, atol=4e-3)
|
||||
|
||||
def test_gower_pairwise(self):
|
||||
"""Test Gower distance with GPU pairwise distance computation"""
|
||||
@@ -474,18 +491,18 @@ class TestAllPairwiseDistance(unittest.TestCase):
|
||||
nq = 10
|
||||
|
||||
rs = np.random.RandomState(123)
|
||||
|
||||
|
||||
# Create mixed data: first half numeric [0,1], second half categorical (negative)
|
||||
xs = np.zeros((nb, d), dtype=np.float32)
|
||||
qs = np.zeros((nq, d), dtype=np.float32)
|
||||
|
||||
|
||||
# Numeric features in [0,1] range
|
||||
xs[:, :d//2] = rs.rand(nb, d//2)
|
||||
qs[:, :d//2] = rs.rand(nq, d//2)
|
||||
|
||||
xs[:, : d // 2] = rs.rand(nb, d // 2)
|
||||
qs[:, : d // 2] = rs.rand(nq, d // 2)
|
||||
|
||||
# Categorical features (negative integers)
|
||||
xs[:, d//2:] = -rs.randint(1, 4, size=(nb, d//2)).astype('float32')
|
||||
qs[:, d//2:] = -rs.randint(1, 4, size=(nq, d//2)).astype('float32')
|
||||
xs[:, d // 2 :] = -rs.randint(1, 4, size=(nb, d // 2)).astype("float32")
|
||||
qs[:, d // 2 :] = -rs.randint(1, 4, size=(nq, d // 2)).astype("float32")
|
||||
|
||||
res = faiss.StandardGpuResources()
|
||||
|
||||
@@ -513,11 +530,10 @@ class TestAllPairwiseDistance(unittest.TestCase):
|
||||
# IndexFlat will sort the results, so we need to do the same on our end
|
||||
out_d = np.sort(out_d, axis=1)
|
||||
|
||||
print('Gower max diff:', np.abs(ref_d - out_d).max())
|
||||
print("Gower max diff:", np.abs(ref_d - out_d).max())
|
||||
np.testing.assert_allclose(ref_d, out_d, atol=1e-5)
|
||||
|
||||
|
||||
|
||||
def eval_codec(q, xb):
|
||||
codes = q.compute_codes(xb)
|
||||
decoded = q.decode(codes)
|
||||
@@ -536,7 +552,7 @@ class TestResidualQuantizer(unittest.TestCase):
|
||||
# know there is a problem with it.
|
||||
@unittest.skip("Skipped due to ncall memory corruption.")
|
||||
def test_with_gpu(self):
|
||||
""" check that we get the same results with a GPU quantizer and a CPU quantizer """
|
||||
"""check that we get the same results with a GPU quantizer and a CPU quantizer"""
|
||||
d = 32
|
||||
nt = 3000
|
||||
nb = 1000
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
from __future__ import (
|
||||
absolute_import,
|
||||
division,
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
||||
import math
|
||||
import unittest
|
||||
@@ -30,8 +35,8 @@ class TestIVFSearchPreassigned(unittest.TestCase):
|
||||
idx_gpu.nprobe = nprobe
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
@@ -40,7 +45,8 @@ class TestIVFSearchPreassigned(unittest.TestCase):
|
||||
q_d, q_i = idx_gpu.quantizer.search(xq, nprobe)
|
||||
|
||||
preassigned_d, preassigned_i = ivf_tools.search_preassigned(
|
||||
idx_gpu, xq, k, q_i, q_d)
|
||||
idx_gpu, xq, k, q_i, q_d
|
||||
)
|
||||
|
||||
# Search using the standard API
|
||||
d, i = idx_gpu.search(xq, k)
|
||||
@@ -60,12 +66,14 @@ class TestIVFSearchPreassigned(unittest.TestCase):
|
||||
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
config.use_cuvs = False
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(res, d, nlist, 4, 8, faiss.METRIC_L2, config)
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, 4, 8, faiss.METRIC_L2, config
|
||||
)
|
||||
idx_gpu.nprobe = nprobe
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
@@ -74,7 +82,8 @@ class TestIVFSearchPreassigned(unittest.TestCase):
|
||||
q_d, q_i = idx_gpu.quantizer.search(xq, nprobe)
|
||||
|
||||
preassigned_d, preassigned_i = ivf_tools.search_preassigned(
|
||||
idx_gpu, xq, k, q_i, q_d)
|
||||
idx_gpu, xq, k, q_i, q_d
|
||||
)
|
||||
|
||||
# Search using the standard API
|
||||
d, i = idx_gpu.search(xq, k)
|
||||
@@ -93,14 +102,17 @@ class TestIVFSearchPreassigned(unittest.TestCase):
|
||||
k = 50
|
||||
|
||||
idx_gpu = faiss.GpuIndexIVFScalarQuantizer(
|
||||
res, d, nlist,
|
||||
res,
|
||||
d,
|
||||
nlist,
|
||||
faiss.ScalarQuantizer.QT_6bit,
|
||||
faiss.METRIC_INNER_PRODUCT)
|
||||
faiss.METRIC_INNER_PRODUCT,
|
||||
)
|
||||
idx_gpu.nprobe = nprobe
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
@@ -109,7 +121,8 @@ class TestIVFSearchPreassigned(unittest.TestCase):
|
||||
q_d, q_i = idx_gpu.quantizer.search(xq, nprobe)
|
||||
|
||||
preassigned_d, preassigned_i = ivf_tools.search_preassigned(
|
||||
idx_gpu, xq, k, q_i, q_d)
|
||||
idx_gpu, xq, k, q_i, q_d
|
||||
)
|
||||
|
||||
# Search using the standard API
|
||||
d, i = idx_gpu.search(xq, k)
|
||||
@@ -132,8 +145,8 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
idx_cpu = faiss.IndexIVFFlat(q, d, nlist)
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
idx_cpu.train(xb)
|
||||
idx_cpu.add(xb)
|
||||
@@ -141,8 +154,10 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
# construct a GPU index using the same trained coarse quantizer
|
||||
# from the CPU index
|
||||
config = faiss.GpuIndexIVFFlatConfig()
|
||||
idx_gpu = faiss.GpuIndexIVFFlat(res, q, d, nlist, faiss.METRIC_L2, config)
|
||||
assert(idx_gpu.is_trained)
|
||||
idx_gpu = faiss.GpuIndexIVFFlat(
|
||||
res, q, d, nlist, faiss.METRIC_L2, config
|
||||
)
|
||||
assert idx_gpu.is_trained
|
||||
idx_gpu.add(xb)
|
||||
|
||||
k = 20
|
||||
@@ -155,7 +170,6 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
self.assertGreaterEqual((i_g == i_c).sum(), i_g.size * 0.9)
|
||||
self.assertTrue(np.allclose(d_g, d_c, rtol=5e-5, atol=5e-5))
|
||||
|
||||
|
||||
def test_ivfsq_pu_coarse(self):
|
||||
res = faiss.StandardGpuResources()
|
||||
d = 128
|
||||
@@ -168,11 +182,12 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
|
||||
q = faiss.IndexFlatL2(d)
|
||||
idx_cpu = faiss.IndexIVFScalarQuantizer(
|
||||
q, d, nlist, qtype, faiss.METRIC_L2, use_residual)
|
||||
q, d, nlist, qtype, faiss.METRIC_L2, use_residual
|
||||
)
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
idx_cpu.train(xb)
|
||||
idx_cpu.add(xb)
|
||||
@@ -180,8 +195,9 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
# construct a GPU index using the same trained coarse quantizer
|
||||
# from the CPU index
|
||||
idx_gpu = faiss.GpuIndexIVFScalarQuantizer(
|
||||
res, q, d, nlist, qtype, faiss.METRIC_L2, use_residual)
|
||||
assert(not idx_gpu.is_trained)
|
||||
res, q, d, nlist, qtype, faiss.METRIC_L2, use_residual
|
||||
)
|
||||
assert not idx_gpu.is_trained
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
|
||||
@@ -208,11 +224,12 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
nprobe_lvl_2 = 10
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
coarse_centroids = rs.rand(nlist_lvl_2, d).astype('float32')
|
||||
coarse_centroids = rs.rand(nlist_lvl_2, d).astype("float32")
|
||||
|
||||
# Construct an IVFFlat index for usage as a coarse quantizer
|
||||
idx_coarse_cpu = faiss.IndexIVFFlat(
|
||||
faiss.IndexFlatL2(d), d, nlist_lvl_1)
|
||||
faiss.IndexFlatL2(d), d, nlist_lvl_1
|
||||
)
|
||||
idx_coarse_cpu.set_direct_map_type(faiss.DirectMap.Hashtable)
|
||||
idx_coarse_cpu.nprobe = nprobe_lvl_1
|
||||
|
||||
@@ -220,12 +237,11 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
idx_coarse_cpu.add(coarse_centroids)
|
||||
idx_coarse_cpu.make_direct_map()
|
||||
|
||||
assert(idx_coarse_cpu.ntotal == nlist_lvl_2)
|
||||
assert idx_coarse_cpu.ntotal == nlist_lvl_2
|
||||
|
||||
idx_cpu = faiss.IndexIVFPQ(
|
||||
idx_coarse_cpu, d, nlist_lvl_2, 4, 8)
|
||||
idx_cpu = faiss.IndexIVFPQ(idx_coarse_cpu, d, nlist_lvl_2, 4, 8)
|
||||
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
idx_cpu.train(xb)
|
||||
idx_cpu.add(xb)
|
||||
idx_cpu.nprobe = nprobe_lvl_2
|
||||
@@ -235,8 +251,9 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
config.use_cuvs = False
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(
|
||||
res, idx_coarse_cpu, d, nlist_lvl_2, 4, 8, faiss.METRIC_L2, config)
|
||||
assert(not idx_gpu.is_trained)
|
||||
res, idx_coarse_cpu, d, nlist_lvl_2, 4, 8, faiss.METRIC_L2, config
|
||||
)
|
||||
assert not idx_gpu.is_trained
|
||||
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
@@ -248,7 +265,7 @@ class TestIVFPluggableCoarseQuantizer(unittest.TestCase):
|
||||
for use_precomputed in [False, True]:
|
||||
idx_gpu.setPrecomputedCodes(use_precomputed)
|
||||
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
d_g, i_g = idx_gpu.search(xq, k)
|
||||
d_c, i_c = idx_cpu.search(xq, k)
|
||||
|
||||
@@ -265,8 +282,8 @@ class TestInterleavedIVFPQLayout(unittest.TestCase):
|
||||
nq = 20
|
||||
|
||||
rs = np.random.RandomState(123)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
nlist = int(math.sqrt(nb))
|
||||
sub_q = 16
|
||||
@@ -274,9 +291,13 @@ class TestInterleavedIVFPQLayout(unittest.TestCase):
|
||||
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
config.interleavedLayout = True
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(res, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2, config)
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2, config
|
||||
)
|
||||
q = faiss.IndexFlatL2(d)
|
||||
idx_cpu = faiss.IndexIVFPQ(q, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2)
|
||||
idx_cpu = faiss.IndexIVFPQ(
|
||||
q, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2
|
||||
)
|
||||
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
@@ -309,8 +330,8 @@ class TestInterleavedIVFPQLayout(unittest.TestCase):
|
||||
nq = 20
|
||||
|
||||
rs = np.random.RandomState(234)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
nlist = int(math.sqrt(nb))
|
||||
sub_q = 16
|
||||
@@ -319,9 +340,13 @@ class TestInterleavedIVFPQLayout(unittest.TestCase):
|
||||
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
config.interleavedLayout = True
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(res, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2, config)
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2, config
|
||||
)
|
||||
q = faiss.IndexFlatL2(d)
|
||||
idx_cpu = faiss.IndexIVFPQ(q, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2)
|
||||
idx_cpu = faiss.IndexIVFPQ(
|
||||
q, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2
|
||||
)
|
||||
|
||||
idx_gpu.train(xb)
|
||||
idx_gpu.add(xb)
|
||||
@@ -353,8 +378,8 @@ class TestInterleavedIVFPQLayout(unittest.TestCase):
|
||||
nq = 20
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
nlist = int(math.sqrt(nb))
|
||||
sub_q = 16
|
||||
@@ -363,9 +388,13 @@ class TestInterleavedIVFPQLayout(unittest.TestCase):
|
||||
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
config.interleavedLayout = True
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(res, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2, config)
|
||||
idx_gpu = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2, config
|
||||
)
|
||||
q = faiss.IndexFlatL2(d)
|
||||
idx_cpu = faiss.IndexIVFPQ(q, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2)
|
||||
idx_cpu = faiss.IndexIVFPQ(
|
||||
q, d, nlist, sub_q, bits_per_code, faiss.METRIC_L2
|
||||
)
|
||||
|
||||
idx_cpu.train(xb)
|
||||
idx_cpu.add(xb)
|
||||
@@ -398,11 +427,11 @@ class TestIVFIndices(unittest.TestCase):
|
||||
nlist = 10
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xb_indices_base = np.arange(nb, dtype=np.int64)
|
||||
|
||||
# Force values to not be representable in int32
|
||||
xb_indices = (xb_indices_base + 4294967296).astype('int64')
|
||||
xb_indices = (xb_indices_base + 4294967296).astype("int64")
|
||||
|
||||
config = faiss.GpuIndexIVFFlatConfig()
|
||||
idx = faiss.GpuIndexIVFFlat(res, d, nlist, faiss.METRIC_L2, config)
|
||||
@@ -432,15 +461,16 @@ class TestIVFIndices(unittest.TestCase):
|
||||
nbits = 8
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xb_indices_base = np.arange(nb, dtype=np.int64)
|
||||
|
||||
# Force values to not be representable in int32
|
||||
xb_indices = (xb_indices_base + 4294967296).astype('int64')
|
||||
xb_indices = (xb_indices_base + 4294967296).astype("int64")
|
||||
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
idx = faiss.GpuIndexIVFPQ(res, d, nlist, M, nbits,
|
||||
faiss.METRIC_L2, config)
|
||||
idx = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, M, nbits, faiss.METRIC_L2, config
|
||||
)
|
||||
idx.train(xb)
|
||||
idx.add_with_ids(xb, xb_indices)
|
||||
|
||||
@@ -451,8 +481,9 @@ class TestIVFIndices(unittest.TestCase):
|
||||
config.indicesOptions = faiss.INDICES_32_BIT
|
||||
# 32-bit indices are not supported with cuVS
|
||||
config.use_cuvs = False
|
||||
idx = faiss.GpuIndexIVFPQ(res, d, nlist, M, nbits,
|
||||
faiss.METRIC_L2, config)
|
||||
idx = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, M, nbits, faiss.METRIC_L2, config
|
||||
)
|
||||
idx.train(xb)
|
||||
idx.add_with_ids(xb, xb_indices)
|
||||
|
||||
@@ -468,15 +499,16 @@ class TestIVFIndices(unittest.TestCase):
|
||||
qtype = faiss.ScalarQuantizer.QT_4bit
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xb_indices_base = np.arange(nb, dtype=np.int64)
|
||||
|
||||
# Force values to not be representable in int32
|
||||
xb_indices = (xb_indices_base + 4294967296).astype('int64')
|
||||
xb_indices = (xb_indices_base + 4294967296).astype("int64")
|
||||
|
||||
config = faiss.GpuIndexIVFScalarQuantizerConfig()
|
||||
idx = faiss.GpuIndexIVFScalarQuantizer(res, d, nlist, qtype,
|
||||
faiss.METRIC_L2, True, config)
|
||||
idx = faiss.GpuIndexIVFScalarQuantizer(
|
||||
res, d, nlist, qtype, faiss.METRIC_L2, True, config
|
||||
)
|
||||
idx.train(xb)
|
||||
idx.add_with_ids(xb, xb_indices)
|
||||
|
||||
@@ -485,8 +517,9 @@ class TestIVFIndices(unittest.TestCase):
|
||||
|
||||
# Store values using 32-bit indices instead
|
||||
config.indicesOptions = faiss.INDICES_32_BIT
|
||||
idx = faiss.GpuIndexIVFScalarQuantizer(res, d, nlist, qtype,
|
||||
faiss.METRIC_L2, True, config)
|
||||
idx = faiss.GpuIndexIVFScalarQuantizer(
|
||||
res, d, nlist, qtype, faiss.METRIC_L2, True, config
|
||||
)
|
||||
idx.train(xb)
|
||||
idx.add_with_ids(xb, xb_indices)
|
||||
|
||||
@@ -518,15 +551,16 @@ class TestInvalidParams(unittest.TestCase):
|
||||
nbits = 8
|
||||
|
||||
rs = np.random.RandomState(567)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xb_indices_base = np.arange(nb, dtype=np.int64)
|
||||
|
||||
# Force values to not be representable in int32
|
||||
xb_indices = (xb_indices_base + 4294967296).astype('int64')
|
||||
xb_indices = (xb_indices_base + 4294967296).astype("int64")
|
||||
|
||||
config = faiss.GpuIndexIVFPQConfig()
|
||||
idx = faiss.GpuIndexIVFPQ(res, d, nlist, M, nbits,
|
||||
faiss.METRIC_L2, config)
|
||||
idx = faiss.GpuIndexIVFPQ(
|
||||
res, d, nlist, M, nbits, faiss.METRIC_L2, config
|
||||
)
|
||||
idx.train(xb)
|
||||
idx.add_with_ids(xb, xb_indices)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import numpy as np
|
||||
class TestGpuIndexIvfflat(unittest.TestCase):
|
||||
def test_reconstruct_n(self):
|
||||
index = faiss.index_factory(4, "IVF10,Flat")
|
||||
x = np.random.RandomState(123).rand(10, 4).astype('float32')
|
||||
x = np.random.RandomState(123).rand(10, 4).astype("float32")
|
||||
index.train(x)
|
||||
index.add(x)
|
||||
res = faiss.StandardGpuResources()
|
||||
|
||||
@@ -11,9 +11,9 @@ import faiss
|
||||
|
||||
def make_t(num, d, clamp=False):
|
||||
rs = np.random.RandomState(123)
|
||||
x = rs.rand(num, d).astype('float32')
|
||||
x = rs.rand(num, d).astype("float32")
|
||||
if clamp:
|
||||
x = (x * 255).astype('uint8').astype('float32')
|
||||
x = (x * 255).astype("uint8").astype("float32")
|
||||
return x
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ def make_indices_copy_from_cpu(nlist, d, qtype, by_residual, metric, clamp):
|
||||
|
||||
quantizer_cp = faiss.IndexFlat(d, metric)
|
||||
idx_cpu = faiss.IndexIVFScalarQuantizer(
|
||||
quantizer_cp, d, nlist, qtype, metric, by_residual)
|
||||
quantizer_cp, d, nlist, qtype, metric, by_residual
|
||||
)
|
||||
|
||||
idx_cpu.train(to_train)
|
||||
idx_cpu.add(to_train)
|
||||
@@ -44,13 +45,15 @@ def make_indices_copy_from_gpu(nlist, d, qtype, by_residual, metric, clamp):
|
||||
config = faiss.GpuIndexIVFScalarQuantizerConfig()
|
||||
config.use_cuvs = False
|
||||
idx_gpu = faiss.GpuIndexIVFScalarQuantizer(
|
||||
res, d, nlist, qtype, metric, by_residual, config)
|
||||
res, d, nlist, qtype, metric, by_residual, config
|
||||
)
|
||||
idx_gpu.train(to_train)
|
||||
idx_gpu.add(to_train)
|
||||
|
||||
quantizer_cp = faiss.IndexFlat(d, metric)
|
||||
idx_cpu = faiss.IndexIVFScalarQuantizer(
|
||||
quantizer_cp, d, nlist, qtype, metric, by_residual)
|
||||
quantizer_cp, d, nlist, qtype, metric, by_residual
|
||||
)
|
||||
idx_gpu.copyTo(idx_cpu)
|
||||
|
||||
return idx_cpu, idx_gpu
|
||||
@@ -61,7 +64,8 @@ def make_indices_train(nlist, d, qtype, by_residual, metric, clamp):
|
||||
|
||||
quantizer_cp = faiss.IndexFlat(d, metric)
|
||||
idx_cpu = faiss.IndexIVFScalarQuantizer(
|
||||
quantizer_cp, d, nlist, qtype, metric, by_residual)
|
||||
quantizer_cp, d, nlist, qtype, metric, by_residual
|
||||
)
|
||||
assert by_residual == idx_cpu.by_residual
|
||||
|
||||
idx_cpu.train(to_train)
|
||||
@@ -72,7 +76,8 @@ def make_indices_train(nlist, d, qtype, by_residual, metric, clamp):
|
||||
config = faiss.GpuIndexIVFScalarQuantizerConfig()
|
||||
config.use_cuvs = False
|
||||
idx_gpu = faiss.GpuIndexIVFScalarQuantizer(
|
||||
res, d, nlist, qtype, metric, by_residual, config)
|
||||
res, d, nlist, qtype, metric, by_residual, config
|
||||
)
|
||||
assert by_residual == idx_gpu.by_residual
|
||||
|
||||
idx_gpu.train(to_train)
|
||||
@@ -80,6 +85,7 @@ def make_indices_train(nlist, d, qtype, by_residual, metric, clamp):
|
||||
|
||||
return idx_cpu, idx_gpu
|
||||
|
||||
|
||||
#
|
||||
# Testing functions
|
||||
#
|
||||
@@ -116,8 +122,8 @@ def compare_results(d1, i1, d2, i2):
|
||||
# Invalid results should be the same for both
|
||||
# (except if we happen to hit different centroids)
|
||||
for inv1, inv2 in zip(invalid1, invalid2):
|
||||
if (len(inv1) != len(inv2)):
|
||||
print('mismatch ', len(inv1), len(inv2), inv2[0])
|
||||
if len(inv1) != len(inv2):
|
||||
print("mismatch ", len(inv1), len(inv2), inv2[0])
|
||||
|
||||
assert len(inv1) == len(inv2)
|
||||
idx_invalid += len(inv2)
|
||||
@@ -150,8 +156,8 @@ def check_diffs(total_num, in_window_thresh, diffs, diff_inf, invalid):
|
||||
if abs(diff) <= diff_window:
|
||||
in_window += diffs[diff] / total_num
|
||||
|
||||
if (in_window < in_window_thresh):
|
||||
print('error {} {}'.format(in_window, in_window_thresh))
|
||||
if in_window < in_window_thresh:
|
||||
print("error {} {}".format(in_window, in_window_thresh))
|
||||
assert in_window >= in_window_thresh
|
||||
|
||||
|
||||
@@ -163,27 +169,30 @@ def do_test_with_index(ci, gi, nprobe, k, clamp, in_window_thresh):
|
||||
gi.nprobe = gi.nprobe
|
||||
|
||||
total_num = num_query * k
|
||||
check_diffs(total_num, in_window_thresh,
|
||||
*compare_results(*ci.search(to_query, k),
|
||||
*gi.search(to_query, k)))
|
||||
check_diffs(
|
||||
total_num,
|
||||
in_window_thresh,
|
||||
*compare_results(*ci.search(to_query, k), *gi.search(to_query, k))
|
||||
)
|
||||
|
||||
|
||||
def do_test(nlist, d, qtype, by_residual, metric, nprobe, k):
|
||||
clamp = (qtype == faiss.ScalarQuantizer.QT_8bit_direct)
|
||||
ci, gi = make_indices_copy_from_cpu(nlist, d, qtype,
|
||||
by_residual, metric, clamp)
|
||||
clamp = qtype == faiss.ScalarQuantizer.QT_8bit_direct
|
||||
ci, gi = make_indices_copy_from_cpu(
|
||||
nlist, d, qtype, by_residual, metric, clamp
|
||||
)
|
||||
# A direct copy should be much more closely in agreement
|
||||
# (except for fp accumulation order differences)
|
||||
do_test_with_index(ci, gi, nprobe, k, clamp, 0.99)
|
||||
|
||||
ci, gi = make_indices_copy_from_gpu(nlist, d, qtype,
|
||||
by_residual, metric, clamp)
|
||||
ci, gi = make_indices_copy_from_gpu(
|
||||
nlist, d, qtype, by_residual, metric, clamp
|
||||
)
|
||||
# A direct copy should be much more closely in agreement
|
||||
# (except for fp accumulation order differences)
|
||||
do_test_with_index(ci, gi, nprobe, k, clamp, 0.99)
|
||||
|
||||
ci, gi = make_indices_train(nlist, d, qtype,
|
||||
by_residual, metric, clamp)
|
||||
ci, gi = make_indices_train(nlist, d, qtype, by_residual, metric, clamp)
|
||||
# Separate training can produce a slightly different coarse quantizer
|
||||
# and residuals
|
||||
do_test_with_index(ci, gi, nprobe, k, clamp, 0.8)
|
||||
@@ -195,15 +204,16 @@ def do_multi_test(qtype):
|
||||
k = 50
|
||||
|
||||
for d in [11, 64, 77]:
|
||||
if (qtype != faiss.ScalarQuantizer.QT_8bit_direct):
|
||||
if qtype != faiss.ScalarQuantizer.QT_8bit_direct:
|
||||
# residual doesn't make sense here
|
||||
do_test(nlist, d, qtype, True,
|
||||
faiss.METRIC_L2, nprobe, k)
|
||||
do_test(nlist, d, qtype, True,
|
||||
faiss.METRIC_INNER_PRODUCT, nprobe, k)
|
||||
do_test(nlist, d, qtype, True, faiss.METRIC_L2, nprobe, k)
|
||||
do_test(
|
||||
nlist, d, qtype, True, faiss.METRIC_INNER_PRODUCT, nprobe, k
|
||||
)
|
||||
do_test(nlist, d, qtype, False, faiss.METRIC_L2, nprobe, k)
|
||||
do_test(nlist, d, qtype, False, faiss.METRIC_INNER_PRODUCT, nprobe, k)
|
||||
|
||||
|
||||
#
|
||||
# Test
|
||||
#
|
||||
|
||||
@@ -80,8 +80,8 @@ class TestRefs(unittest.TestCase):
|
||||
|
||||
def test_GpuIndexIVFPQ(self):
|
||||
index_to_delete = faiss.IndexIVFPQ(
|
||||
faiss.IndexFlatL2(self.d),
|
||||
self.d, self.nlist, 2, 8)
|
||||
faiss.IndexFlatL2(self.d), self.d, self.nlist, 2, 8
|
||||
)
|
||||
idx = faiss.GpuIndexIVFPQ(self.res, index_to_delete)
|
||||
do_multi_test(idx, index_to_delete, self.db)
|
||||
|
||||
@@ -92,7 +92,7 @@ class TestRefs(unittest.TestCase):
|
||||
self.nlist,
|
||||
faiss.ScalarQuantizer.QT_8bit_direct,
|
||||
faiss.METRIC_L2,
|
||||
False
|
||||
False,
|
||||
)
|
||||
idx = faiss.GpuIndexIVFScalarQuantizer(self.res, index_to_delete)
|
||||
do_multi_test(idx, index_to_delete, self.db)
|
||||
|
||||
@@ -10,7 +10,7 @@ import faiss
|
||||
|
||||
def make_t(num, d):
|
||||
rs = np.random.RandomState(123)
|
||||
return rs.rand(num, d).astype('float32')
|
||||
return rs.rand(num, d).astype("float32")
|
||||
|
||||
|
||||
class TestGpuSerialize(unittest.TestCase):
|
||||
@@ -38,10 +38,22 @@ class TestGpuSerialize(unittest.TestCase):
|
||||
# IVFSQ
|
||||
config = faiss.GpuIndexIVFScalarQuantizerConfig()
|
||||
config.use_cuvs = False
|
||||
indexes.append(faiss.GpuIndexIVFScalarQuantizer(res, d, nlist, faiss.ScalarQuantizer.QT_fp16, faiss.METRIC_L2, True, config))
|
||||
indexes.append(
|
||||
faiss.GpuIndexIVFScalarQuantizer(
|
||||
res,
|
||||
d,
|
||||
nlist,
|
||||
faiss.ScalarQuantizer.QT_fp16,
|
||||
faiss.METRIC_L2,
|
||||
True,
|
||||
config,
|
||||
)
|
||||
)
|
||||
|
||||
# IVFPQ
|
||||
indexes.append(faiss.GpuIndexIVFPQ(res, d, nlist, 4, 8, faiss.METRIC_L2))
|
||||
indexes.append(
|
||||
faiss.GpuIndexIVFPQ(res, d, nlist, 4, 8, faiss.METRIC_L2)
|
||||
)
|
||||
|
||||
for index in indexes:
|
||||
index.train(train)
|
||||
@@ -51,11 +63,13 @@ class TestGpuSerialize(unittest.TestCase):
|
||||
|
||||
ser = faiss.serialize_index(faiss.index_gpu_to_cpu(index))
|
||||
cpu_index = faiss.deserialize_index(ser)
|
||||
|
||||
|
||||
gpu_cloner_options = faiss.GpuClonerOptions()
|
||||
if isinstance(index, faiss.GpuIndexIVFScalarQuantizer):
|
||||
gpu_cloner_options.use_cuvs = False
|
||||
gpu_index_restore = faiss.index_cpu_to_gpu(res, 0, cpu_index, gpu_cloner_options)
|
||||
gpu_index_restore = faiss.index_cpu_to_gpu(
|
||||
res, 0, cpu_index, gpu_cloner_options
|
||||
)
|
||||
|
||||
restore_d, restore_i = gpu_index_restore.search(query, k)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestMoveToGpu(unittest.TestCase):
|
||||
def create_index(self, factory_string):
|
||||
dimension = 128
|
||||
n = 2500
|
||||
db_vectors = np.random.random((n, dimension)).astype('float32')
|
||||
db_vectors = np.random.random((n, dimension)).astype("float32")
|
||||
index = faiss.index_factory(dimension, factory_string)
|
||||
index.train(db_vectors)
|
||||
if factory_string.startswith("IDMap"):
|
||||
@@ -26,9 +26,9 @@ class TestMoveToGpu(unittest.TestCase):
|
||||
index.add(db_vectors)
|
||||
return index
|
||||
|
||||
def create_and_clone(self, factory_string,
|
||||
allowCpuCoarseQuantizer=None,
|
||||
use_cuvs=None):
|
||||
def create_and_clone(
|
||||
self, factory_string, allowCpuCoarseQuantizer=None, use_cuvs=None
|
||||
):
|
||||
idx = self.create_index(factory_string)
|
||||
config = faiss.GpuClonerOptions()
|
||||
if allowCpuCoarseQuantizer is not None:
|
||||
@@ -42,20 +42,25 @@ class TestMoveToGpu(unittest.TestCase):
|
||||
self.create_and_clone(factory_string)
|
||||
except Exception as e:
|
||||
if "not implemented" not in str(e):
|
||||
self.fail("Expected an exception but no exception was "
|
||||
"thrown for factory_string: %s." % factory_string)
|
||||
self.fail(
|
||||
"Expected an exception but no exception was "
|
||||
"thrown for factory_string: %s." % factory_string
|
||||
)
|
||||
|
||||
def verify_clones_successfully(self, factory_string,
|
||||
allowCpuCoarseQuantizer=None,
|
||||
use_cuvs=None):
|
||||
def verify_clones_successfully(
|
||||
self, factory_string, allowCpuCoarseQuantizer=None, use_cuvs=None
|
||||
):
|
||||
try:
|
||||
self.create_and_clone(
|
||||
factory_string,
|
||||
allowCpuCoarseQuantizer=allowCpuCoarseQuantizer,
|
||||
use_cuvs=use_cuvs)
|
||||
use_cuvs=use_cuvs,
|
||||
)
|
||||
except Exception as e:
|
||||
self.fail("Unexpected exception thrown factory_string: "
|
||||
"%s; error message: %s." % (factory_string, str(e)))
|
||||
self.fail(
|
||||
"Unexpected exception thrown factory_string: "
|
||||
"%s; error message: %s." % (factory_string, str(e))
|
||||
)
|
||||
|
||||
def test_not_implemented_indices(self):
|
||||
self.verify_throws_not_implemented_exception("PQ16")
|
||||
@@ -76,19 +81,21 @@ class TestMoveToGpu(unittest.TestCase):
|
||||
|
||||
# set use_cuvs to false, these index types are not supported on cuVS
|
||||
self.verify_clones_successfully("IVF32,SQ8", use_cuvs=False)
|
||||
self.verify_clones_successfully(
|
||||
"PCA32,IVF32,SQ8", use_cuvs=False)
|
||||
self.verify_clones_successfully("PCA32,IVF32,SQ8", use_cuvs=False)
|
||||
|
||||
def test_with_flag(self):
|
||||
self.verify_clones_successfully("IVF32_HNSW,Flat",
|
||||
allowCpuCoarseQuantizer=True)
|
||||
self.verify_clones_successfully("IVF256(PQ2x4fs),Flat",
|
||||
allowCpuCoarseQuantizer=True)
|
||||
self.verify_clones_successfully(
|
||||
"IVF32_HNSW,Flat", allowCpuCoarseQuantizer=True
|
||||
)
|
||||
self.verify_clones_successfully(
|
||||
"IVF256(PQ2x4fs),Flat", allowCpuCoarseQuantizer=True
|
||||
)
|
||||
|
||||
def test_with_flag_set_to_false(self):
|
||||
try:
|
||||
self.verify_clones_successfully("IVF32_HNSW,Flat",
|
||||
allowCpuCoarseQuantizer=False)
|
||||
self.verify_clones_successfully(
|
||||
"IVF32_HNSW,Flat", allowCpuCoarseQuantizer=False
|
||||
)
|
||||
except Exception as e:
|
||||
if "set the flag to true to allow the CPU fallback" not in str(e):
|
||||
self.fail("Unexepected error message thrown: %s." % str(e))
|
||||
|
||||
@@ -9,8 +9,8 @@ import faiss
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
"CUVS" not in faiss.get_compile_options(),
|
||||
"only if cuVS is compiled in")
|
||||
"CUVS" not in faiss.get_compile_options(), "only if cuVS is compiled in"
|
||||
)
|
||||
class TestIVFPQSearchCagraConfigDtypes(unittest.TestCase):
|
||||
"""Regression tests for the SWIG int typemap on cudaDataType_t.
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ class TestShardedFlat(unittest.TestCase):
|
||||
nq = 200
|
||||
k = 10
|
||||
rs = np.random.RandomState(123)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
index_cpu = faiss.IndexFlatL2(d)
|
||||
|
||||
@@ -58,10 +58,11 @@ class TestShardedFlat(unittest.TestCase):
|
||||
def do_test_sharded_ivf(self, index_key):
|
||||
ds = SyntheticDataset(32, 8000, 10000, 100)
|
||||
index = faiss.index_factory(ds.d, index_key)
|
||||
if 'HNSW' in index_key:
|
||||
if "HNSW" in index_key:
|
||||
# make a bit more reproducible...
|
||||
faiss.ParameterSpace().set_index_parameter(
|
||||
index, 'quantizer_efSearch', 40)
|
||||
index, "quantizer_efSearch", 40
|
||||
)
|
||||
index.train(ds.get_train())
|
||||
index.add(ds.get_database())
|
||||
Dref, Iref = index.search(ds.get_queries(), 10)
|
||||
@@ -83,7 +84,7 @@ class TestShardedFlat(unittest.TestCase):
|
||||
np.testing.assert_array_almost_equal(Dref, Dnew, decimal=4)
|
||||
|
||||
# the nprobe is taken from the sub-indexes
|
||||
faiss.GpuParameterSpace().set_index_parameter(index, 'nprobe', 8)
|
||||
faiss.GpuParameterSpace().set_index_parameter(index, "nprobe", 8)
|
||||
Dnew8, Inew8 = index.search(ds.get_queries(), 10)
|
||||
np.testing.assert_array_equal(Iref8, Inew8)
|
||||
np.testing.assert_array_almost_equal(Dref8, Dnew8, decimal=4)
|
||||
@@ -154,14 +155,13 @@ class EvalIVFPQAccuracy(unittest.TestCase):
|
||||
qc = q[:d1, :]
|
||||
|
||||
def make_mat(n):
|
||||
return np.dot(
|
||||
np.random.random(size=(nb, d1)), qc).astype('float32')
|
||||
return np.dot(np.random.random(size=(nb, d1)), qc).astype("float32")
|
||||
|
||||
return (make_mat(nt), make_mat(nb), make_mat(nq))
|
||||
|
||||
def test_mm(self):
|
||||
# trouble with MKL+fbmake that appears only at runtime. Check it here
|
||||
x = np.random.random(size=(100, 20)).astype('float32')
|
||||
x = np.random.random(size=(100, 20)).astype("float32")
|
||||
mat = faiss.PCAMatrix(20, 10)
|
||||
mat.train(x)
|
||||
mat.apply_py(x)
|
||||
@@ -185,7 +185,7 @@ class EvalIVFPQAccuracy(unittest.TestCase):
|
||||
# adding some ids because there was a bug in this case;
|
||||
# those need to be cast to idx_t(= int64_t), because
|
||||
# on windows the numpy int default is int32
|
||||
ids = (np.arange(nb) * 3 + 12345).astype('int64')
|
||||
ids = (np.arange(nb) * 3 + 12345).astype("int64")
|
||||
index.add_with_ids(xb, ids)
|
||||
ts.append(time.time())
|
||||
|
||||
@@ -203,15 +203,15 @@ class EvalIVFPQAccuracy(unittest.TestCase):
|
||||
mem_info = res.getMemoryInfo()
|
||||
|
||||
assert isinstance(mem_info, dict)
|
||||
assert isinstance(mem_info[0]['FlatData'], tuple)
|
||||
assert isinstance(mem_info[0]['FlatData'][0], int)
|
||||
assert isinstance(mem_info[0]['FlatData'][1], int)
|
||||
assert isinstance(mem_info[0]["FlatData"], tuple)
|
||||
assert isinstance(mem_info[0]["FlatData"][0], int)
|
||||
assert isinstance(mem_info[0]["FlatData"][1], int)
|
||||
|
||||
gpu_index.nprobe = 4
|
||||
|
||||
Dnew, Inew = gpu_index.search(xq, 10)
|
||||
ts.append(time.time())
|
||||
print('times:', [t - ts[0] for t in ts])
|
||||
print("times:", [t - ts[0] for t in ts])
|
||||
|
||||
# Give us some margin of error
|
||||
self.assertGreaterEqual((Iref == Inew).sum(), Iref.size - 50)
|
||||
@@ -230,7 +230,8 @@ class EvalIVFPQAccuracy(unittest.TestCase):
|
||||
gpu_index = faiss.index_cpu_to_gpu_multiple_py(res, index, co)
|
||||
|
||||
faiss.GpuParameterSpace().set_index_parameter(
|
||||
gpu_index, 'nprobe', 4)
|
||||
gpu_index, "nprobe", 4
|
||||
)
|
||||
|
||||
Dnew, Inew = gpu_index.search(xq, 10)
|
||||
|
||||
@@ -239,10 +240,10 @@ class EvalIVFPQAccuracy(unittest.TestCase):
|
||||
self.assertGreaterEqual((Iref == Inew).sum(), Iref.size * 0.99)
|
||||
|
||||
def test_cpu_to_gpu_IVFPQ(self):
|
||||
self.do_cpu_to_gpu('IVF128,PQ4')
|
||||
self.do_cpu_to_gpu("IVF128,PQ4")
|
||||
|
||||
def test_cpu_to_gpu_IVFFlat(self):
|
||||
self.do_cpu_to_gpu('IVF128,Flat')
|
||||
self.do_cpu_to_gpu("IVF128,Flat")
|
||||
|
||||
def test_set_gpu_param(self):
|
||||
index = faiss.index_factory(12, "PCAR8,IVF10,PQ4")
|
||||
|
||||
@@ -14,7 +14,7 @@ from faiss.contrib.torch import clustering
|
||||
|
||||
|
||||
def to_column_major_torch(x):
|
||||
if hasattr(torch, 'contiguous_format'):
|
||||
if hasattr(torch, "contiguous_format"):
|
||||
return x.t().clone(memory_format=torch.contiguous_format).t()
|
||||
else:
|
||||
# was default setting before memory_format was introduced
|
||||
@@ -35,7 +35,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
cpu_index.add(xb_torch.numpy())
|
||||
|
||||
# Add to CPU index with torch GPU (should fail)
|
||||
xb_torch_gpu = torch.rand(10000, 128, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
xb_torch_gpu = torch.rand(
|
||||
10000, 128, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
with self.assertRaises(AssertionError):
|
||||
cpu_index.add(xb_torch_gpu)
|
||||
|
||||
@@ -59,8 +61,12 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(i_torch_cpu.cuda(), i_torch_gpu))
|
||||
|
||||
# Search with torch GPU using pre-allocated arrays
|
||||
new_d_torch_gpu = torch.zeros(10, 10, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
new_i_torch_gpu = torch.zeros(10, 10, device=torch.device('cuda', 0), dtype=torch.int64)
|
||||
new_d_torch_gpu = torch.zeros(
|
||||
10, 10, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
new_i_torch_gpu = torch.zeros(
|
||||
10, 10, device=torch.device("cuda", 0), dtype=torch.int64
|
||||
)
|
||||
gpu_index.search(xq_torch_gpu, 10, new_d_torch_gpu, new_i_torch_gpu)
|
||||
|
||||
self.assertTrue(torch.equal(d_torch_cpu.cuda(), new_d_torch_gpu))
|
||||
@@ -87,10 +93,17 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
config.use_cuvs = False
|
||||
|
||||
index = faiss.GpuIndexIVFFlat(res, d, nlist, faiss.METRIC_L2, config)
|
||||
xb = torch.rand(1000, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
xb = torch.rand(
|
||||
1000, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
index.train(xb)
|
||||
|
||||
ids = torch.arange(1000, 1000 + xb.shape[0], device=torch.device('cuda', 0), dtype=torch.int64)
|
||||
ids = torch.arange(
|
||||
1000,
|
||||
1000 + xb.shape[0],
|
||||
device=torch.device("cuda", 0),
|
||||
dtype=torch.int64,
|
||||
)
|
||||
|
||||
# Test add_with_ids with torch gpu
|
||||
index.add_with_ids(xb, ids)
|
||||
@@ -124,7 +137,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
res.noTempMemory()
|
||||
index = faiss.GpuIndexFlatL2(res, d)
|
||||
|
||||
xb = torch.rand(100, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
xb = torch.rand(
|
||||
100, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
index.add(xb)
|
||||
|
||||
# Test reconstruct with torch gpu (native return)
|
||||
@@ -133,7 +148,7 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(xb[7], y))
|
||||
|
||||
# Test reconstruct with numpy output provided
|
||||
y = np.empty(d, dtype='float32')
|
||||
y = np.empty(d, dtype="float32")
|
||||
index.reconstruct(11, y)
|
||||
self.assertTrue(np.array_equal(xb.cpu().numpy()[11], y))
|
||||
|
||||
@@ -143,7 +158,7 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(xb[12].cpu(), y))
|
||||
|
||||
# Test reconstruct with torch gpu output providesd
|
||||
y = torch.empty(d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
y = torch.empty(d, device=torch.device("cuda", 0), dtype=torch.float32)
|
||||
index.reconstruct(13, y)
|
||||
self.assertTrue(torch.equal(xb[13], y))
|
||||
|
||||
@@ -153,7 +168,7 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(xb[10:20], y))
|
||||
|
||||
# Test reconstruct with numpy output provided
|
||||
y = np.empty((10, d), dtype='float32')
|
||||
y = np.empty((10, d), dtype="float32")
|
||||
index.reconstruct_n(20, 10, y)
|
||||
self.assertTrue(np.array_equal(xb.cpu().numpy()[20:30], y))
|
||||
|
||||
@@ -163,7 +178,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(xb[40:50].cpu(), y))
|
||||
|
||||
# Test reconstruct_n with torch gpu output provided
|
||||
y = torch.empty(10, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
y = torch.empty(
|
||||
10, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
index.reconstruct_n(50, 10, y)
|
||||
self.assertTrue(torch.equal(xb[50:60], y))
|
||||
|
||||
@@ -177,7 +194,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
|
||||
index = faiss.GpuIndexIVFFlat(res, d, nlist, faiss.METRIC_L2, config)
|
||||
|
||||
xb = torch.rand(100, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
xb = torch.rand(
|
||||
100, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
index.train(xb)
|
||||
index.add(xb)
|
||||
|
||||
@@ -187,7 +206,7 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(xb[10:20], y))
|
||||
|
||||
# Test reconstruct with numpy output provided
|
||||
y = np.empty((10, d), dtype='float32')
|
||||
y = np.empty((10, d), dtype="float32")
|
||||
index.reconstruct_n(20, 10, y)
|
||||
self.assertTrue(np.array_equal(xb.cpu().numpy()[20:30], y))
|
||||
|
||||
@@ -197,7 +216,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(xb[40:50].cpu(), y))
|
||||
|
||||
# Test reconstruct_n with torch gpu output provided
|
||||
y = torch.empty(10, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
y = torch.empty(
|
||||
10, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
index.reconstruct_n(50, 10, y)
|
||||
self.assertTrue(torch.equal(xb[50:60], y))
|
||||
|
||||
@@ -208,7 +229,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
res.noTempMemory()
|
||||
|
||||
index = faiss.GpuIndexFlatL2(res, d)
|
||||
xb = torch.rand(10000, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
xb = torch.rand(
|
||||
10000, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
index.add(xb)
|
||||
|
||||
index_cpu = faiss.IndexFlatL2(d)
|
||||
@@ -216,7 +239,9 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
|
||||
# Test assign with native gpu output
|
||||
# both input as gpu torch and input as cpu torch
|
||||
xq = torch.rand(10, d, device=torch.device('cuda', 0), dtype=torch.float32)
|
||||
xq = torch.rand(
|
||||
10, d, device=torch.device("cuda", 0), dtype=torch.float32
|
||||
)
|
||||
|
||||
labels = index.assign(xq, 5)
|
||||
labels_cpu = index_cpu.assign(xq.cpu(), 5)
|
||||
@@ -228,7 +253,7 @@ class TestTorchUtilsGPU(unittest.TestCase):
|
||||
self.assertTrue(np.array_equal(labels, labels_cpu))
|
||||
|
||||
# Test assign with numpy output provided
|
||||
labels = np.empty((xq.shape[0], 5), dtype='int64')
|
||||
labels = np.empty((xq.shape[0], 5), dtype="int64")
|
||||
index.assign(xq.cpu().numpy(), 5, labels)
|
||||
self.assertTrue(np.array_equal(labels, labels_cpu))
|
||||
|
||||
@@ -300,7 +325,9 @@ class TestTorchUtilsKnnGpu(unittest.TestCase):
|
||||
D, I = faiss.knn_gpu(res, xq_c, xb_c, k, use_cuvs=use_cuvs)
|
||||
|
||||
self.assertTrue(torch.equal(torch.from_numpy(I), gt_I))
|
||||
self.assertLess((torch.from_numpy(D) - gt_D).abs().max(), 1e-4)
|
||||
self.assertLess(
|
||||
(torch.from_numpy(D) - gt_D).abs().max(), 1e-4
|
||||
)
|
||||
|
||||
# test torch (cpu, gpu) inputs
|
||||
for is_cuda in True, False:
|
||||
@@ -323,7 +350,9 @@ class TestTorchUtilsKnnGpu(unittest.TestCase):
|
||||
xb_c = to_column_major_torch(xb)
|
||||
assert not xb_c.is_contiguous()
|
||||
|
||||
D, I = faiss.knn_gpu(res, xq_c, xb_c, k, use_cuvs=use_cuvs)
|
||||
D, I = faiss.knn_gpu(
|
||||
res, xq_c, xb_c, k, use_cuvs=use_cuvs
|
||||
)
|
||||
|
||||
self.assertTrue(torch.equal(I.cpu(), gt_I))
|
||||
self.assertLess((D.cpu() - gt_D).abs().max(), 1e-4)
|
||||
@@ -331,7 +360,9 @@ class TestTorchUtilsKnnGpu(unittest.TestCase):
|
||||
# test on subset
|
||||
try:
|
||||
# This internally uses the current pytorch stream
|
||||
D, I = faiss.knn_gpu(res, xq_c[6:8], xb_c, k, use_cuvs=use_cuvs)
|
||||
D, I = faiss.knn_gpu(
|
||||
res, xq_c[6:8], xb_c, k, use_cuvs=use_cuvs
|
||||
)
|
||||
except TypeError:
|
||||
if not xq_row_major:
|
||||
# then it is expected
|
||||
@@ -343,8 +374,8 @@ class TestTorchUtilsKnnGpu(unittest.TestCase):
|
||||
self.assertLess((D.cpu() - gt_D[6:8]).abs().max(), 1e-4)
|
||||
|
||||
@unittest.skipUnless(
|
||||
"CUVS" in faiss.get_compile_options(),
|
||||
"only if CUVS is compiled in")
|
||||
"CUVS" in faiss.get_compile_options(), "only if CUVS is compiled in"
|
||||
)
|
||||
def test_knn_gpu_cuvs(self):
|
||||
self.test_knn_gpu(use_cuvs=True)
|
||||
|
||||
@@ -434,7 +465,9 @@ class TestTorchUtilsPairwiseDistanceGpu(unittest.TestCase):
|
||||
# do the same on our end
|
||||
D = np.sort(D, axis=1)
|
||||
|
||||
self.assertLess((torch.from_numpy(D) - gt_D).abs().max(), 1e-4)
|
||||
self.assertLess(
|
||||
(torch.from_numpy(D) - gt_D).abs().max(), 1e-4
|
||||
)
|
||||
|
||||
# test torch (cpu, gpu) inputs
|
||||
for is_cuda in True, False:
|
||||
@@ -468,7 +501,9 @@ class TestTorchUtilsPairwiseDistanceGpu(unittest.TestCase):
|
||||
# test on subset
|
||||
try:
|
||||
# This internally uses the current pytorch stream
|
||||
D = faiss.pairwise_distance_gpu(res, xq_c[4:8], xb_c)
|
||||
D = faiss.pairwise_distance_gpu(
|
||||
res, xq_c[4:8], xb_c
|
||||
)
|
||||
except TypeError:
|
||||
if not xq_row_major:
|
||||
# then it is expected
|
||||
@@ -487,7 +522,7 @@ class TestTorchUtilsPairwiseDistanceGpu(unittest.TestCase):
|
||||
class TestClustering(unittest.TestCase):
|
||||
|
||||
def test_python_kmeans(self):
|
||||
""" Test the python implementation of kmeans """
|
||||
"""Test the python implementation of kmeans"""
|
||||
ds = datasets.SyntheticDataset(32, 10000, 0, 0)
|
||||
x = ds.get_train()
|
||||
|
||||
|
||||
@@ -123,7 +123,8 @@ class TestMetalIVFFlat(unittest.TestCase):
|
||||
|
||||
quantizer = faiss.IndexFlatIP(d)
|
||||
cpu_index = faiss.IndexIVFFlat(
|
||||
quantizer, d, nlist, faiss.METRIC_INNER_PRODUCT)
|
||||
quantizer, d, nlist, faiss.METRIC_INNER_PRODUCT
|
||||
)
|
||||
cpu_index.nprobe = nprobe
|
||||
cpu_index.train(xb)
|
||||
cpu_index.add(xb)
|
||||
|
||||
@@ -390,7 +390,9 @@ def search_with_parameters(index, x, k, params=None, output_stats=False):
|
||||
range_search_with_parameters_c = range_search_with_parameters
|
||||
|
||||
|
||||
def range_search_with_parameters(index, x, radius, params=None, output_stats=False):
|
||||
def range_search_with_parameters(
|
||||
index, x, radius, params=None, output_stats=False
|
||||
):
|
||||
x = np.ascontiguousarray(x, dtype="float32")
|
||||
n, d = x.shape
|
||||
assert d == index.d
|
||||
|
||||
@@ -19,6 +19,7 @@ from faiss.loader import *
|
||||
# classes from the SWIG interface
|
||||
###########################################
|
||||
|
||||
|
||||
def _make_deprecated_swig_class(deprecated_name, base_name):
|
||||
"""
|
||||
Dynamically construct deprecated classes as wrappers around renamed ones
|
||||
@@ -62,58 +63,59 @@ def _make_deprecated_swig_class(deprecated_name, base_name):
|
||||
# numpy array / std::vector conversions
|
||||
###########################################
|
||||
|
||||
sizeof_long = array.array('l').itemsize
|
||||
sizeof_long = array.array("l").itemsize
|
||||
deprecated_name_map = {
|
||||
# deprecated: replacement
|
||||
'Float': 'Float32',
|
||||
'Double': 'Float64',
|
||||
'Char': 'Int8',
|
||||
'Int': 'Int32',
|
||||
'Long': 'Int32' if sizeof_long == 4 else 'Int64',
|
||||
'LongLong': 'Int64',
|
||||
'Byte': 'UInt8',
|
||||
"Float": "Float32",
|
||||
"Double": "Float64",
|
||||
"Char": "Int8",
|
||||
"Int": "Int32",
|
||||
"Long": "Int32" if sizeof_long == 4 else "Int64",
|
||||
"LongLong": "Int64",
|
||||
"Byte": "UInt8",
|
||||
# previously misspelled variant
|
||||
'Uint64': 'UInt64',
|
||||
"Uint64": "UInt64",
|
||||
}
|
||||
|
||||
for depr_prefix, base_prefix in deprecated_name_map.items():
|
||||
_make_deprecated_swig_class(depr_prefix + "Vector", base_prefix + "Vector")
|
||||
|
||||
# same for the three legacy *VectorVector classes
|
||||
if depr_prefix in ['Float', 'Long', 'Byte']:
|
||||
_make_deprecated_swig_class(depr_prefix + "VectorVector",
|
||||
base_prefix + "VectorVector")
|
||||
if depr_prefix in ["Float", "Long", "Byte"]:
|
||||
_make_deprecated_swig_class(
|
||||
depr_prefix + "VectorVector", base_prefix + "VectorVector"
|
||||
)
|
||||
|
||||
# mapping from vector names in swigfaiss.swig and the numpy dtype names
|
||||
# TODO: once deprecated classes are removed, remove the dict and just use .lower() below
|
||||
vector_name_map = {
|
||||
'Float32': 'float32',
|
||||
'Float64': 'float64',
|
||||
'Int8': 'int8',
|
||||
'Int16': 'int16',
|
||||
'Int32': 'int32',
|
||||
'Int64': 'int64',
|
||||
'UInt8': 'uint8',
|
||||
'UInt16': 'uint16',
|
||||
'UInt32': 'uint32',
|
||||
'UInt64': 'uint64',
|
||||
**{k: v.lower() for k, v in deprecated_name_map.items()}
|
||||
"Float32": "float32",
|
||||
"Float64": "float64",
|
||||
"Int8": "int8",
|
||||
"Int16": "int16",
|
||||
"Int32": "int32",
|
||||
"Int64": "int64",
|
||||
"UInt8": "uint8",
|
||||
"UInt16": "uint16",
|
||||
"UInt32": "uint32",
|
||||
"UInt64": "uint64",
|
||||
**{k: v.lower() for k, v in deprecated_name_map.items()},
|
||||
}
|
||||
|
||||
|
||||
def vector_to_array(v):
|
||||
""" convert a C++ vector to a numpy array """
|
||||
"""convert a C++ vector to a numpy array"""
|
||||
classname = v.__class__.__name__
|
||||
if classname.startswith('AlignedTable'):
|
||||
if classname.startswith("AlignedTable"):
|
||||
return AlignedTable_to_array(v)
|
||||
if classname.startswith('MaybeOwnedVector'):
|
||||
if classname.startswith("MaybeOwnedVector"):
|
||||
dtype = np.dtype(vector_name_map[classname[16:]])
|
||||
a = np.empty(v.size(), dtype=dtype)
|
||||
if v.size() > 0:
|
||||
memcpy(swig_ptr(a), v.data(), a.nbytes)
|
||||
return a
|
||||
|
||||
assert classname.endswith('Vector')
|
||||
assert classname.endswith("Vector")
|
||||
dtype = np.dtype(vector_name_map[classname[:-6]])
|
||||
a = np.empty(v.size(), dtype=dtype)
|
||||
if v.size() > 0:
|
||||
@@ -126,34 +128,41 @@ def vector_float_to_array(v):
|
||||
|
||||
|
||||
def copy_array_to_vector(a, v):
|
||||
""" copy a numpy array to a vector """
|
||||
n, = a.shape
|
||||
"""copy a numpy array to a vector"""
|
||||
(n,) = a.shape
|
||||
classname = v.__class__.__name__
|
||||
if classname.startswith('MaybeOwnedVector'):
|
||||
assert v.is_owned, 'cannot copy to an non-owned MaybeOwnedVector'
|
||||
if classname.startswith("MaybeOwnedVector"):
|
||||
assert v.is_owned, "cannot copy to an non-owned MaybeOwnedVector"
|
||||
dtype = np.dtype(vector_name_map[classname[16:]])
|
||||
assert dtype == a.dtype, (
|
||||
'cannot copy a %s array to a %s (should be %s)' % (
|
||||
a.dtype, classname, dtype))
|
||||
assert (
|
||||
dtype == a.dtype
|
||||
), "cannot copy a %s array to a %s (should be %s)" % (
|
||||
a.dtype,
|
||||
classname,
|
||||
dtype,
|
||||
)
|
||||
v.resize(n)
|
||||
if n > 0:
|
||||
memcpy(v.data(), swig_ptr(a), a.nbytes)
|
||||
return
|
||||
|
||||
assert classname.endswith('Vector')
|
||||
assert classname.endswith("Vector")
|
||||
dtype = np.dtype(vector_name_map[classname[:-6]])
|
||||
assert dtype == a.dtype, (
|
||||
'cannot copy a %s array to a %s (should be %s)' % (
|
||||
a.dtype, classname, dtype))
|
||||
assert dtype == a.dtype, "cannot copy a %s array to a %s (should be %s)" % (
|
||||
a.dtype,
|
||||
classname,
|
||||
dtype,
|
||||
)
|
||||
v.resize(n)
|
||||
if n > 0:
|
||||
memcpy(v.data(), swig_ptr(a), a.nbytes)
|
||||
|
||||
|
||||
# same for AlignedTable
|
||||
|
||||
|
||||
def copy_array_to_AlignedTable(a, v):
|
||||
n, = a.shape
|
||||
(n,) = a.shape
|
||||
# TODO check class name
|
||||
assert v.itemsize() == a.itemsize
|
||||
v.resize(n)
|
||||
@@ -162,9 +171,9 @@ def copy_array_to_AlignedTable(a, v):
|
||||
|
||||
|
||||
def array_to_AlignedTable(a):
|
||||
if a.dtype == 'uint16':
|
||||
if a.dtype == "uint16":
|
||||
v = AlignedTableUint16(a.size)
|
||||
elif a.dtype == 'uint8':
|
||||
elif a.dtype == "uint8":
|
||||
v = AlignedTableUint8(a.size)
|
||||
else:
|
||||
assert False
|
||||
@@ -173,9 +182,9 @@ def array_to_AlignedTable(a):
|
||||
|
||||
|
||||
def AlignedTable_to_array(v):
|
||||
""" convert an AlignedTable to a numpy array """
|
||||
"""convert an AlignedTable to a numpy array"""
|
||||
classname = v.__class__.__name__
|
||||
assert classname.startswith('AlignedTable')
|
||||
assert classname.startswith("AlignedTable")
|
||||
dtype = classname[12:].lower()
|
||||
a = np.empty(v.size(), dtype=dtype)
|
||||
if a.size > 0:
|
||||
|
||||
+293
-205
File diff suppressed because it is too large
Load Diff
+160
-114
@@ -25,10 +25,10 @@ import collections.abc
|
||||
def kmin(array, k):
|
||||
"""return k smallest values (and their indices) of the lines of a
|
||||
float32 array"""
|
||||
array = np.ascontiguousarray(array, dtype='float32')
|
||||
array = np.ascontiguousarray(array, dtype="float32")
|
||||
m, n = array.shape
|
||||
I = np.zeros((m, k), dtype='int64')
|
||||
D = np.zeros((m, k), dtype='float32')
|
||||
I = np.zeros((m, k), dtype="int64")
|
||||
D = np.zeros((m, k), dtype="float32")
|
||||
ha = faiss.float_maxheap_array_t()
|
||||
ha.ids = swig_ptr(I)
|
||||
ha.val = swig_ptr(D)
|
||||
@@ -43,10 +43,10 @@ def kmin(array, k):
|
||||
def kmax(array, k):
|
||||
"""return k largest values (and their indices) of the lines of a
|
||||
float32 array"""
|
||||
array = np.ascontiguousarray(array, dtype='float32')
|
||||
array = np.ascontiguousarray(array, dtype="float32")
|
||||
m, n = array.shape
|
||||
I = np.zeros((m, k), dtype='int64')
|
||||
D = np.zeros((m, k), dtype='float32')
|
||||
I = np.zeros((m, k), dtype="int64")
|
||||
D = np.zeros((m, k), dtype="float32")
|
||||
ha = faiss.float_minheap_array_t()
|
||||
ha.ids = swig_ptr(I)
|
||||
ha.val = swig_ptr(D)
|
||||
@@ -61,36 +61,38 @@ def kmax(array, k):
|
||||
def pairwise_distances(xq, xb, metric=METRIC_L2, metric_arg=0):
|
||||
"""compute the whole pairwise distance matrix between two sets of
|
||||
vectors"""
|
||||
xq = np.ascontiguousarray(xq, dtype='float32')
|
||||
xb = np.ascontiguousarray(xb, dtype='float32')
|
||||
xq = np.ascontiguousarray(xq, dtype="float32")
|
||||
xb = np.ascontiguousarray(xb, dtype="float32")
|
||||
nq, d = xq.shape
|
||||
nb, d2 = xb.shape
|
||||
assert d == d2
|
||||
dis = np.empty((nq, nb), dtype='float32')
|
||||
dis = np.empty((nq, nb), dtype="float32")
|
||||
if metric == METRIC_L2:
|
||||
pairwise_L2sqr(
|
||||
d, nq, swig_ptr(xq),
|
||||
nb, swig_ptr(xb),
|
||||
swig_ptr(dis))
|
||||
pairwise_L2sqr(d, nq, swig_ptr(xq), nb, swig_ptr(xb), swig_ptr(dis))
|
||||
elif metric == METRIC_INNER_PRODUCT:
|
||||
dis[:] = xq @ xb.T
|
||||
else:
|
||||
pairwise_extra_distances(
|
||||
d, nq, swig_ptr(xq),
|
||||
nb, swig_ptr(xb),
|
||||
metric, metric_arg,
|
||||
swig_ptr(dis))
|
||||
d,
|
||||
nq,
|
||||
swig_ptr(xq),
|
||||
nb,
|
||||
swig_ptr(xb),
|
||||
metric,
|
||||
metric_arg,
|
||||
swig_ptr(dis),
|
||||
)
|
||||
return dis
|
||||
|
||||
|
||||
def rand(n, seed=12345):
|
||||
res = np.empty(n, dtype='float32')
|
||||
res = np.empty(n, dtype="float32")
|
||||
float_rand(swig_ptr(res), res.size, seed)
|
||||
return res
|
||||
|
||||
|
||||
def randint(n, seed=12345, vmax=None):
|
||||
res = np.empty(n, dtype='int64')
|
||||
res = np.empty(n, dtype="int64")
|
||||
if vmax is None:
|
||||
int64_rand(swig_ptr(res), res.size, seed)
|
||||
else:
|
||||
@@ -102,48 +104,53 @@ lrand = randint
|
||||
|
||||
|
||||
def randn(n, seed=12345):
|
||||
res = np.empty(n, dtype='float32')
|
||||
res = np.empty(n, dtype="float32")
|
||||
float_randn(swig_ptr(res), res.size, seed)
|
||||
return res
|
||||
|
||||
|
||||
def checksum(a):
|
||||
""" compute a checksum for quick-and-dirty comparisons of arrays """
|
||||
a = a.view('uint8')
|
||||
"""compute a checksum for quick-and-dirty comparisons of arrays"""
|
||||
a = a.view("uint8")
|
||||
if a.ndim == 1:
|
||||
return bvec_checksum(a.size, swig_ptr(a))
|
||||
n, d = a.shape
|
||||
cs = np.zeros(n, dtype='uint64')
|
||||
cs = np.zeros(n, dtype="uint64")
|
||||
bvecs_checksum(n, d, swig_ptr(a), swig_ptr(cs))
|
||||
return cs
|
||||
|
||||
|
||||
rand_smooth_vectors_c = rand_smooth_vectors
|
||||
|
||||
|
||||
def rand_smooth_vectors(n, d, seed=1234):
|
||||
res = np.empty((n, d), dtype='float32')
|
||||
res = np.empty((n, d), dtype="float32")
|
||||
rand_smooth_vectors_c(n, d, swig_ptr(res), seed)
|
||||
return res
|
||||
|
||||
|
||||
def eval_intersection(I1, I2):
|
||||
""" size of intersection between each line of two result tables"""
|
||||
I1 = np.ascontiguousarray(I1, dtype='int64')
|
||||
I2 = np.ascontiguousarray(I2, dtype='int64')
|
||||
"""size of intersection between each line of two result tables"""
|
||||
I1 = np.ascontiguousarray(I1, dtype="int64")
|
||||
I2 = np.ascontiguousarray(I2, dtype="int64")
|
||||
n = I1.shape[0]
|
||||
assert I2.shape[0] == n
|
||||
k1, k2 = I1.shape[1], I2.shape[1]
|
||||
ninter = 0
|
||||
for i in range(n):
|
||||
ninter += ranklist_intersection_size(
|
||||
k1, swig_ptr(I1[i]), k2, swig_ptr(I2[i]))
|
||||
k1, swig_ptr(I1[i]), k2, swig_ptr(I2[i])
|
||||
)
|
||||
return ninter
|
||||
|
||||
|
||||
def normalize_L2(x):
|
||||
fvec_renorm_L2(x.shape[1], x.shape[0], swig_ptr(x))
|
||||
|
||||
|
||||
bucket_sort_c = bucket_sort
|
||||
|
||||
|
||||
def bucket_sort(tab, nbucket=None, nt=0):
|
||||
"""Perform a bucket sort on a table of integers.
|
||||
|
||||
@@ -166,17 +173,22 @@ def bucket_sort(tab, nbucket=None, nt=0):
|
||||
tab = np.ascontiguousarray(tab, dtype="int64")
|
||||
if nbucket is None:
|
||||
nbucket = int(tab.max() + 1)
|
||||
lims = np.empty(nbucket + 1, dtype='int64')
|
||||
perm = np.empty(tab.size, dtype='int64')
|
||||
lims = np.empty(nbucket + 1, dtype="int64")
|
||||
perm = np.empty(tab.size, dtype="int64")
|
||||
bucket_sort_c(
|
||||
tab.size, faiss.swig_ptr(tab.view('uint64')),
|
||||
nbucket, faiss.swig_ptr(lims), faiss.swig_ptr(perm),
|
||||
nt
|
||||
tab.size,
|
||||
faiss.swig_ptr(tab.view("uint64")),
|
||||
nbucket,
|
||||
faiss.swig_ptr(lims),
|
||||
faiss.swig_ptr(perm),
|
||||
nt,
|
||||
)
|
||||
return lims, perm
|
||||
|
||||
|
||||
matrix_bucket_sort_inplace_c = matrix_bucket_sort_inplace
|
||||
|
||||
|
||||
def matrix_bucket_sort_inplace(tab, nbucket=None, nt=0):
|
||||
"""Perform a bucket sort on a matrix, recording the original
|
||||
row of each element.
|
||||
@@ -199,15 +211,13 @@ def matrix_bucket_sort_inplace(tab, nbucket=None, nt=0):
|
||||
lims : array_like
|
||||
cumulative sum of bucket sizes (size vmax + 1)
|
||||
"""
|
||||
assert tab.dtype == 'int32' or tab.dtype == 'int64'
|
||||
assert tab.dtype == "int32" or tab.dtype == "int64"
|
||||
nrow, ncol = tab.shape
|
||||
if nbucket is None:
|
||||
nbucket = int(tab.max() + 1)
|
||||
lims = np.empty(nbucket + 1, dtype='int64')
|
||||
lims = np.empty(nbucket + 1, dtype="int64")
|
||||
matrix_bucket_sort_inplace_c(
|
||||
nrow, ncol, faiss.swig_ptr(tab),
|
||||
nbucket, faiss.swig_ptr(lims),
|
||||
nt
|
||||
nrow, ncol, faiss.swig_ptr(tab), nbucket, faiss.swig_ptr(lims), nt
|
||||
)
|
||||
return lims
|
||||
|
||||
@@ -216,6 +226,7 @@ def matrix_bucket_sort_inplace(tab, nbucket=None, nt=0):
|
||||
# ResultHeap
|
||||
###########################################
|
||||
|
||||
|
||||
class ResultHeap:
|
||||
"""Accumulate query results from a sliced dataset. The final result will
|
||||
be in self.D, self.I."""
|
||||
@@ -226,8 +237,8 @@ class ResultHeap:
|
||||
k: number of results per query
|
||||
keep_max: keep the top-k maximum values instead of the minima
|
||||
"""
|
||||
self.I = np.zeros((nq, k), dtype='int64')
|
||||
self.D = np.zeros((nq, k), dtype='float32')
|
||||
self.I = np.zeros((nq, k), dtype="int64")
|
||||
self.D = np.zeros((nq, k), dtype="float32")
|
||||
self.nq, self.k = nq, k
|
||||
if keep_max:
|
||||
heaps = float_minheap_array_t()
|
||||
@@ -247,13 +258,11 @@ class ResultHeap:
|
||||
D, I do not need to be in a particular order (heap or sorted)
|
||||
"""
|
||||
nq, kd = D.shape
|
||||
D = np.ascontiguousarray(D, dtype='float32')
|
||||
I = np.ascontiguousarray(I, dtype='int64')
|
||||
D = np.ascontiguousarray(D, dtype="float32")
|
||||
I = np.ascontiguousarray(I, dtype="int64")
|
||||
assert I.shape == (nq, kd)
|
||||
assert nq == self.nq
|
||||
self.heaps.addn_with_ids(
|
||||
kd, swig_ptr(D),
|
||||
swig_ptr(I), kd)
|
||||
self.heaps.addn_with_ids(kd, swig_ptr(D), swig_ptr(I), kd)
|
||||
|
||||
def add_result_subset(self, subset, D, I):
|
||||
"""
|
||||
@@ -264,16 +273,17 @@ class ResultHeap:
|
||||
nsubset, kd = D.shape
|
||||
assert nsubset == len(subset)
|
||||
assert (
|
||||
I.ndim == 2 and D.shape == I.shape or
|
||||
I.ndim == 1 and I.shape == (kd, )
|
||||
I.ndim == 2
|
||||
and D.shape == I.shape
|
||||
or I.ndim == 1
|
||||
and I.shape == (kd,)
|
||||
)
|
||||
D = np.ascontiguousarray(D, dtype='float32')
|
||||
I = np.ascontiguousarray(I, dtype='int64')
|
||||
subset = np.ascontiguousarray(subset, dtype='int64')
|
||||
D = np.ascontiguousarray(D, dtype="float32")
|
||||
I = np.ascontiguousarray(I, dtype="int64")
|
||||
subset = np.ascontiguousarray(subset, dtype="int64")
|
||||
id_stride = 0 if I.ndim == 1 else kd
|
||||
self.heaps.addn_query_subset_with_ids(
|
||||
nsubset, swig_ptr(subset),
|
||||
kd, swig_ptr(D), swig_ptr(I), id_stride
|
||||
nsubset, swig_ptr(subset), kd, swig_ptr(D), swig_ptr(I), id_stride
|
||||
)
|
||||
|
||||
def finalize(self):
|
||||
@@ -292,44 +302,62 @@ def merge_knn_results(Dall, Iall, keep_max=False):
|
||||
Inew = np.empty((n, k), dtype=Iall.dtype)
|
||||
func = merge_knn_results_CMax if keep_max else merge_knn_results_CMin
|
||||
func(
|
||||
n, k, nshard,
|
||||
swig_ptr(Dall), swig_ptr(Iall),
|
||||
swig_ptr(Dnew), swig_ptr(Inew)
|
||||
n,
|
||||
k,
|
||||
nshard,
|
||||
swig_ptr(Dall),
|
||||
swig_ptr(Iall),
|
||||
swig_ptr(Dnew),
|
||||
swig_ptr(Inew),
|
||||
)
|
||||
return Dnew, Inew
|
||||
|
||||
|
||||
######################################################
|
||||
# Efficient ID to ID map
|
||||
######################################################
|
||||
|
||||
|
||||
class MapInt64ToInt64:
|
||||
|
||||
def __init__(self, capacity):
|
||||
self.log2_capacity = int(np.log2(capacity))
|
||||
assert capacity == 2 ** self.log2_capacity, "need power of 2 capacity"
|
||||
assert capacity == 2**self.log2_capacity, "need power of 2 capacity"
|
||||
self.capacity = capacity
|
||||
self.tab = np.empty((capacity, 2), dtype='int64')
|
||||
faiss.hashtable_int64_to_int64_init(self.log2_capacity, swig_ptr(self.tab))
|
||||
self.tab = np.empty((capacity, 2), dtype="int64")
|
||||
faiss.hashtable_int64_to_int64_init(
|
||||
self.log2_capacity, swig_ptr(self.tab)
|
||||
)
|
||||
|
||||
def add(self, keys, vals):
|
||||
n, = keys.shape
|
||||
(n,) = keys.shape
|
||||
assert vals.shape == (n,)
|
||||
faiss.hashtable_int64_to_int64_add(
|
||||
self.log2_capacity, swig_ptr(self.tab),
|
||||
n, swig_ptr(keys), swig_ptr(vals))
|
||||
self.log2_capacity,
|
||||
swig_ptr(self.tab),
|
||||
n,
|
||||
swig_ptr(keys),
|
||||
swig_ptr(vals),
|
||||
)
|
||||
|
||||
def lookup(self, keys):
|
||||
n, = keys.shape
|
||||
vals = np.empty((n,), dtype='int64')
|
||||
(n,) = keys.shape
|
||||
vals = np.empty((n,), dtype="int64")
|
||||
faiss.hashtable_int64_to_int64_lookup(
|
||||
self.log2_capacity, swig_ptr(self.tab),
|
||||
n, swig_ptr(keys), swig_ptr(vals))
|
||||
self.log2_capacity,
|
||||
swig_ptr(self.tab),
|
||||
n,
|
||||
swig_ptr(keys),
|
||||
swig_ptr(vals),
|
||||
)
|
||||
return vals
|
||||
|
||||
|
||||
######################################################
|
||||
# KNN function
|
||||
######################################################
|
||||
|
||||
|
||||
def knn(xq, xb, k, metric=METRIC_L2, metric_arg=0.0):
|
||||
"""
|
||||
Compute the k nearest neighbors of a vector without constructing an index
|
||||
@@ -355,30 +383,35 @@ def knn(xq, xb, k, metric=METRIC_L2, metric_arg=0.0):
|
||||
I : array_like
|
||||
Labels of the nearest neighbors, shape (nq, k)
|
||||
"""
|
||||
xq = np.ascontiguousarray(xq, dtype='float32')
|
||||
xb = np.ascontiguousarray(xb, dtype='float32')
|
||||
xq = np.ascontiguousarray(xq, dtype="float32")
|
||||
xb = np.ascontiguousarray(xb, dtype="float32")
|
||||
nq, d = xq.shape
|
||||
nb, d2 = xb.shape
|
||||
assert d == d2
|
||||
|
||||
I = np.empty((nq, k), dtype='int64')
|
||||
D = np.empty((nq, k), dtype='float32')
|
||||
I = np.empty((nq, k), dtype="int64")
|
||||
D = np.empty((nq, k), dtype="float32")
|
||||
|
||||
if metric == METRIC_L2:
|
||||
knn_L2sqr(
|
||||
swig_ptr(xq), swig_ptr(xb),
|
||||
d, nq, nb, k, swig_ptr(D), swig_ptr(I)
|
||||
swig_ptr(xq), swig_ptr(xb), d, nq, nb, k, swig_ptr(D), swig_ptr(I)
|
||||
)
|
||||
elif metric == METRIC_INNER_PRODUCT:
|
||||
knn_inner_product(
|
||||
swig_ptr(xq), swig_ptr(xb),
|
||||
d, nq, nb, k, swig_ptr(D), swig_ptr(I)
|
||||
swig_ptr(xq), swig_ptr(xb), d, nq, nb, k, swig_ptr(D), swig_ptr(I)
|
||||
)
|
||||
else:
|
||||
else:
|
||||
knn_extra_metrics(
|
||||
swig_ptr(xq), swig_ptr(xb),
|
||||
d, nq, nb, metric, metric_arg, k,
|
||||
swig_ptr(D), swig_ptr(I)
|
||||
swig_ptr(xq),
|
||||
swig_ptr(xb),
|
||||
d,
|
||||
nq,
|
||||
nb,
|
||||
metric,
|
||||
metric_arg,
|
||||
k,
|
||||
swig_ptr(D),
|
||||
swig_ptr(I),
|
||||
)
|
||||
|
||||
return D, I
|
||||
@@ -412,8 +445,8 @@ def knn_hamming(xq, xb, k, variant="hc"):
|
||||
nq, d = xq.shape
|
||||
nb, d2 = xb.shape
|
||||
assert d == d2
|
||||
D = np.empty((nq, k), dtype='int32')
|
||||
I = np.empty((nq, k), dtype='int64')
|
||||
D = np.empty((nq, k), dtype="int32")
|
||||
I = np.empty((nq, k), dtype="int64")
|
||||
|
||||
if variant == "hc":
|
||||
heap = faiss.int_maxheap_array_t()
|
||||
@@ -422,13 +455,18 @@ def knn_hamming(xq, xb, k, variant="hc"):
|
||||
heap.ids = faiss.swig_ptr(I)
|
||||
heap.val = faiss.swig_ptr(D)
|
||||
faiss.hammings_knn_hc(
|
||||
heap, faiss.swig_ptr(xq), faiss.swig_ptr(xb), nb,
|
||||
d, 1
|
||||
heap, faiss.swig_ptr(xq), faiss.swig_ptr(xb), nb, d, 1
|
||||
)
|
||||
elif variant == "mc":
|
||||
faiss.hammings_knn_mc(
|
||||
faiss.swig_ptr(xq), faiss.swig_ptr(xb), nq, nb, k, d,
|
||||
faiss.swig_ptr(D), faiss.swig_ptr(I)
|
||||
faiss.swig_ptr(xq),
|
||||
faiss.swig_ptr(xb),
|
||||
nq,
|
||||
nb,
|
||||
k,
|
||||
d,
|
||||
faiss.swig_ptr(D),
|
||||
faiss.swig_ptr(I),
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
@@ -487,8 +525,8 @@ class Kmeans:
|
||||
|
||||
def __init__(self, d, k, **kwargs):
|
||||
"""d: input dimension, k: nb of centroids. Additional
|
||||
parameters are passed on the ClusteringParameters object,
|
||||
including niter=25, verbose=False, spherical = False
|
||||
parameters are passed on the ClusteringParameters object,
|
||||
including niter=25, verbose=False, spherical = False
|
||||
"""
|
||||
self.d = d
|
||||
self.reset(k)
|
||||
@@ -498,7 +536,7 @@ class Kmeans:
|
||||
else:
|
||||
self.cp = ClusteringParameters()
|
||||
for k, v in kwargs.items():
|
||||
if k == 'gpu':
|
||||
if k == "gpu":
|
||||
if v == True or v == -1:
|
||||
v = get_num_gpus()
|
||||
self.gpu = v
|
||||
@@ -516,7 +554,9 @@ class Kmeans:
|
||||
else:
|
||||
self.index = IndexFlatL2(d)
|
||||
if self.gpu:
|
||||
self.index = faiss.index_cpu_to_all_gpus(self.index, ngpu=self.gpu)
|
||||
self.index = faiss.index_cpu_to_all_gpus(
|
||||
self.index, ngpu=self.gpu
|
||||
)
|
||||
else:
|
||||
if self.gpu:
|
||||
fac = GpuProgressiveDimIndexFactory(ngpu=self.gpu)
|
||||
@@ -525,8 +565,8 @@ class Kmeans:
|
||||
self.fac = fac
|
||||
|
||||
def reset(self, k=None):
|
||||
""" prepare k-means object to perform a new clustering, possibly
|
||||
with another number of centroids """
|
||||
"""prepare k-means object to perform a new clustering, possibly
|
||||
with another number of centroids"""
|
||||
if k is not None:
|
||||
self.k = int(k)
|
||||
self.centroids = None
|
||||
@@ -534,7 +574,7 @@ class Kmeans:
|
||||
self.iteration_stats = None
|
||||
|
||||
def train(self, x, weights=None, init_centroids=None):
|
||||
""" Perform k-means clustering.
|
||||
"""Perform k-means clustering.
|
||||
On output of the function call:
|
||||
|
||||
- the centroids are in the centroids field of size (`k`, `d`).
|
||||
@@ -559,7 +599,7 @@ class Kmeans:
|
||||
final optimization objective
|
||||
|
||||
"""
|
||||
x = np.ascontiguousarray(x, dtype='float32')
|
||||
x = np.ascontiguousarray(x, dtype="float32")
|
||||
n, d = x.shape
|
||||
assert d == self.d
|
||||
|
||||
@@ -569,7 +609,9 @@ class Kmeans:
|
||||
if init_centroids is not None:
|
||||
nc, d2 = init_centroids.shape
|
||||
assert d2 == d
|
||||
faiss.copy_array_to_vector(init_centroids.ravel(), clus.centroids)
|
||||
faiss.copy_array_to_vector(
|
||||
init_centroids.ravel(), clus.centroids
|
||||
)
|
||||
clus.train(x, self.index, weights)
|
||||
else:
|
||||
# not supported for progressive dim
|
||||
@@ -586,15 +628,14 @@ class Kmeans:
|
||||
stats = [stats.at(i) for i in range(stats.size())]
|
||||
self.obj = np.array([st.obj for st in stats])
|
||||
# copy all the iteration_stats objects to a python array
|
||||
stat_fields = 'obj time time_search imbalance_factor nsplit'.split()
|
||||
stat_fields = "obj time time_search imbalance_factor nsplit".split()
|
||||
self.iteration_stats = [
|
||||
{field: getattr(st, field) for field in stat_fields}
|
||||
for st in stats
|
||||
{field: getattr(st, field) for field in stat_fields} for st in stats
|
||||
]
|
||||
return self.obj[-1] if self.obj.size > 0 else 0.0
|
||||
|
||||
def assign(self, x):
|
||||
x = np.ascontiguousarray(x, dtype='float32')
|
||||
x = np.ascontiguousarray(x, dtype="float32")
|
||||
assert self.centroids is not None, "should train before assigning"
|
||||
self.index.reset()
|
||||
self.index.add(self.centroids)
|
||||
@@ -629,9 +670,10 @@ class SuperKmeans(Kmeans):
|
||||
|
||||
def train(self, x, weights=None, init_centroids=None):
|
||||
assert weights is None, "SuperKmeans does not support weights"
|
||||
assert init_centroids is None, \
|
||||
"SuperKmeans does not support init_centroids"
|
||||
x = np.ascontiguousarray(x, dtype='float32')
|
||||
assert (
|
||||
init_centroids is None
|
||||
), "SuperKmeans does not support init_centroids"
|
||||
x = np.ascontiguousarray(x, dtype="float32")
|
||||
n, d = x.shape
|
||||
assert d == self.d
|
||||
|
||||
@@ -643,10 +685,9 @@ class SuperKmeans(Kmeans):
|
||||
stats = sc.iteration_stats
|
||||
stats = [stats.at(i) for i in range(stats.size())]
|
||||
self.obj = np.array([st.obj for st in stats])
|
||||
stat_fields = 'obj time time_search imbalance_factor nsplit'.split()
|
||||
stat_fields = "obj time time_search imbalance_factor nsplit".split()
|
||||
self.iteration_stats = [
|
||||
{field: getattr(st, field) for field in stat_fields}
|
||||
for st in stats
|
||||
{field: getattr(st, field) for field in stat_fields} for st in stats
|
||||
]
|
||||
self.gemm_pruning_rates = faiss.vector_to_array(sc.gemm_pruning_rates)
|
||||
return self.obj[-1] if self.obj.size > 0 else 0.0
|
||||
@@ -656,11 +697,14 @@ class SuperKmeans(Kmeans):
|
||||
# Packing and unpacking bitstrings
|
||||
###########################################
|
||||
|
||||
|
||||
def is_sequence(x):
|
||||
return isinstance(x, collections.abc.Sequence)
|
||||
|
||||
|
||||
pack_bitstrings_c = pack_bitstrings
|
||||
|
||||
|
||||
def pack_bitstrings(a, nbit):
|
||||
"""
|
||||
Pack a set integers (i, j) where i=0:n and j=0:M into
|
||||
@@ -672,22 +716,25 @@ def pack_bitstrings(a, nbit):
|
||||
If nbit is an array: entry (i, j) takes nbit[j] bits.
|
||||
"""
|
||||
n, M = a.shape
|
||||
a = np.ascontiguousarray(a, dtype='int32')
|
||||
a = np.ascontiguousarray(a, dtype="int32")
|
||||
if is_sequence(nbit):
|
||||
nbit = np.ascontiguousarray(nbit, dtype='int32')
|
||||
nbit = np.ascontiguousarray(nbit, dtype="int32")
|
||||
assert nbit.shape == (M,)
|
||||
code_size = int((nbit.sum() + 7) // 8)
|
||||
b = np.empty((n, code_size), dtype='uint8')
|
||||
b = np.empty((n, code_size), dtype="uint8")
|
||||
pack_bitstrings_c(
|
||||
n, M, swig_ptr(nbit), swig_ptr(a), swig_ptr(b), code_size)
|
||||
n, M, swig_ptr(nbit), swig_ptr(a), swig_ptr(b), code_size
|
||||
)
|
||||
else:
|
||||
code_size = (M * nbit + 7) // 8
|
||||
b = np.empty((n, code_size), dtype='uint8')
|
||||
b = np.empty((n, code_size), dtype="uint8")
|
||||
pack_bitstrings_c(n, M, nbit, swig_ptr(a), swig_ptr(b), code_size)
|
||||
return b
|
||||
|
||||
|
||||
unpack_bitstrings_c = unpack_bitstrings
|
||||
|
||||
|
||||
def unpack_bitstrings(b, M_or_nbits, nbit=None):
|
||||
"""
|
||||
Unpack a set integers (i, j) where i=0:n and j=0:M from
|
||||
@@ -703,19 +750,18 @@ def unpack_bitstrings(b, M_or_nbits, nbit=None):
|
||||
"""
|
||||
n, code_size = b.shape
|
||||
if nbit is None:
|
||||
nbit = np.ascontiguousarray(M_or_nbits, dtype='int32')
|
||||
nbit = np.ascontiguousarray(M_or_nbits, dtype="int32")
|
||||
M = len(nbit)
|
||||
min_code_size = int((nbit.sum() + 7) // 8)
|
||||
assert code_size >= min_code_size
|
||||
a = np.empty((n, M), dtype='int32')
|
||||
a = np.empty((n, M), dtype="int32")
|
||||
unpack_bitstrings_c(
|
||||
n, M, swig_ptr(nbit),
|
||||
swig_ptr(b), code_size, swig_ptr(a))
|
||||
n, M, swig_ptr(nbit), swig_ptr(b), code_size, swig_ptr(a)
|
||||
)
|
||||
else:
|
||||
M = M_or_nbits
|
||||
min_code_size = (M * nbit + 7) // 8
|
||||
assert code_size >= min_code_size
|
||||
a = np.empty((n, M), dtype='int32')
|
||||
unpack_bitstrings_c(
|
||||
n, M, nbit, swig_ptr(b), code_size, swig_ptr(a))
|
||||
a = np.empty((n, M), dtype="int32")
|
||||
unpack_bitstrings_c(n, M, nbit, swig_ptr(b), code_size, swig_ptr(a))
|
||||
return a
|
||||
|
||||
@@ -19,9 +19,9 @@ from faiss.loader import *
|
||||
|
||||
|
||||
def index_cpu_to_gpu_multiple_py(resources, index, co=None, gpus=None):
|
||||
""" builds the C++ vectors for the GPU indices and the
|
||||
"""builds the C++ vectors for the GPU indices and the
|
||||
resources. Handles the case where the resources are assigned to
|
||||
the list of GPUs """
|
||||
the list of GPUs"""
|
||||
if gpus is None:
|
||||
gpus = range(len(resources))
|
||||
vres = GpuResourcesVector()
|
||||
@@ -41,7 +41,7 @@ def index_cpu_to_all_gpus(index, co=None, ngpu=-1):
|
||||
|
||||
|
||||
def index_cpu_to_gpus_list(index, co=None, gpus=None, ngpu=-1):
|
||||
""" Here we can pass list of GPU ids as a parameter or ngpu to
|
||||
"""Here we can pass list of GPU ids as a parameter or ngpu to
|
||||
use first n GPU's. gpus mut be a list or None.
|
||||
co is a GpuMultipleClonerOptions
|
||||
"""
|
||||
@@ -53,10 +53,23 @@ def index_cpu_to_gpus_list(index, co=None, gpus=None, ngpu=-1):
|
||||
index_gpu = index_cpu_to_gpu_multiple_py(res, index, co, gpus)
|
||||
return index_gpu
|
||||
|
||||
|
||||
# allows numpy ndarray usage with bfKnn
|
||||
|
||||
|
||||
def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuvs=False, vectorsMemoryLimit=0, queriesMemoryLimit=0):
|
||||
def knn_gpu(
|
||||
res,
|
||||
xq,
|
||||
xb,
|
||||
k,
|
||||
D=None,
|
||||
I=None,
|
||||
metric=METRIC_L2,
|
||||
device=-1,
|
||||
use_cuvs=False,
|
||||
vectorsMemoryLimit=0,
|
||||
queriesMemoryLimit=0,
|
||||
):
|
||||
"""
|
||||
Compute the k nearest neighbors of a vector on one GPU without constructing an index
|
||||
|
||||
@@ -107,7 +120,7 @@ def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuv
|
||||
xq = xq.T
|
||||
xq_row_major = False
|
||||
else:
|
||||
xq = np.ascontiguousarray(xq, dtype='float32')
|
||||
xq = np.ascontiguousarray(xq, dtype="float32")
|
||||
xq_row_major = True
|
||||
|
||||
xq_ptr = swig_ptr(xq)
|
||||
@@ -117,7 +130,7 @@ def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuv
|
||||
elif xq.dtype == np.float16:
|
||||
xq_type = DistanceDataType_F16
|
||||
else:
|
||||
raise TypeError('xq must be f32 or f16')
|
||||
raise TypeError("xq must be f32 or f16")
|
||||
|
||||
nb, d2 = xb.shape
|
||||
assert d2 == d
|
||||
@@ -127,7 +140,7 @@ def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuv
|
||||
xb = xb.T
|
||||
xb_row_major = False
|
||||
else:
|
||||
xb = np.ascontiguousarray(xb, dtype='float32')
|
||||
xb = np.ascontiguousarray(xb, dtype="float32")
|
||||
xb_row_major = True
|
||||
|
||||
xb_ptr = swig_ptr(xb)
|
||||
@@ -137,7 +150,7 @@ def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuv
|
||||
elif xb.dtype == np.float16:
|
||||
xb_type = DistanceDataType_F16
|
||||
else:
|
||||
raise TypeError('xb must be float32 or float16')
|
||||
raise TypeError("xb must be float32 or float16")
|
||||
|
||||
if D is None:
|
||||
D = np.empty((nq, k), dtype=np.float32)
|
||||
@@ -160,7 +173,7 @@ def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuv
|
||||
elif I.dtype == I.dtype == np.int32:
|
||||
I_type = IndicesDataType_I32
|
||||
else:
|
||||
raise TypeError('I must be i64 or i32')
|
||||
raise TypeError("I must be i64 or i32")
|
||||
|
||||
args = GpuDistanceParams()
|
||||
args.metric = metric
|
||||
@@ -189,6 +202,7 @@ def knn_gpu(res, xq, xb, k, D=None, I=None, metric=METRIC_L2, device=-1, use_cuv
|
||||
|
||||
return D, I
|
||||
|
||||
|
||||
# allows numpy ndarray usage with bfKnn for all pairwise distances
|
||||
|
||||
|
||||
@@ -229,8 +243,7 @@ def pairwise_distance_gpu(res, xq, xb, D=None, metric=METRIC_L2, device=-1):
|
||||
xq = xq.T
|
||||
xq_row_major = False
|
||||
else:
|
||||
raise TypeError(
|
||||
'xq matrix should be row (C) or column-major (Fortran)')
|
||||
raise TypeError("xq matrix should be row (C) or column-major (Fortran)")
|
||||
|
||||
xq_ptr = swig_ptr(xq)
|
||||
|
||||
@@ -239,7 +252,7 @@ def pairwise_distance_gpu(res, xq, xb, D=None, metric=METRIC_L2, device=-1):
|
||||
elif xq.dtype == np.float16:
|
||||
xq_type = DistanceDataType_F16
|
||||
else:
|
||||
xq = np.ascontiguousarray(xb, dtype='float32')
|
||||
xq = np.ascontiguousarray(xb, dtype="float32")
|
||||
xq_row_major = True
|
||||
|
||||
nb, d2 = xb.shape
|
||||
@@ -250,7 +263,7 @@ def pairwise_distance_gpu(res, xq, xb, D=None, metric=METRIC_L2, device=-1):
|
||||
xb = xb.T
|
||||
xb_row_major = False
|
||||
else:
|
||||
xb = np.ascontiguousarray(xb, dtype='float32')
|
||||
xb = np.ascontiguousarray(xb, dtype="float32")
|
||||
xb_row_major = True
|
||||
|
||||
xb_ptr = swig_ptr(xb)
|
||||
@@ -260,7 +273,7 @@ def pairwise_distance_gpu(res, xq, xb, D=None, metric=METRIC_L2, device=-1):
|
||||
elif xb.dtype == np.float16:
|
||||
xb_type = DistanceDataType_F16
|
||||
else:
|
||||
raise TypeError('xb must be float32 or float16')
|
||||
raise TypeError("xb must be float32 or float16")
|
||||
|
||||
if D is None:
|
||||
D = np.empty((nq, nb), dtype=np.float32)
|
||||
|
||||
+41
-11
@@ -37,23 +37,34 @@ def supported_instruction_sets():
|
||||
return False
|
||||
# Numpy 2.0 supports SVE detection by __cpu_features__, so just skip
|
||||
import numpy
|
||||
|
||||
if Version(numpy.__version__) >= Version("2.0"):
|
||||
return False
|
||||
# platform-dependent legacy fallback using numpy.distutils.cpuinfo
|
||||
try:
|
||||
import numpy.distutils.cpuinfo
|
||||
return "sve" in numpy.distutils.cpuinfo.cpu.info[0].get('Features', "").split()
|
||||
|
||||
return (
|
||||
"sve"
|
||||
in numpy.distutils.cpuinfo.cpu.info[0]
|
||||
.get("Features", "")
|
||||
.split()
|
||||
)
|
||||
except ImportError:
|
||||
# check if SVE is supported by checking the auxval
|
||||
# using values defined as:
|
||||
# #define AT_HWCAP 16
|
||||
# #define HWCAP_SVE (1 << 22)
|
||||
return bool(__import__('ctypes').CDLL(None).getauxval(16) & (1<<22))
|
||||
return bool(
|
||||
__import__("ctypes").CDLL(None).getauxval(16) & (1 << 22)
|
||||
)
|
||||
|
||||
import numpy
|
||||
|
||||
if Version(numpy.__version__) >= Version("1.19"):
|
||||
# use private API as next-best thing until numpy/numpy#18058 is solved
|
||||
from numpy._core._multiarray_umath import __cpu_features__
|
||||
|
||||
# __cpu_features__ is a dictionary with CPU features
|
||||
# as keys, and True / False as values
|
||||
supported = {k for k, v in __cpu_features__.items() if v}
|
||||
@@ -65,16 +76,24 @@ def supported_instruction_sets():
|
||||
|
||||
# platform-dependent legacy fallback before numpy 1.19, no windows
|
||||
if platform.system() == "Darwin":
|
||||
if subprocess.check_output(["/usr/sbin/sysctl", "hw.optional.avx2_0"])[-1] == '1':
|
||||
if (
|
||||
subprocess.check_output(["/usr/sbin/sysctl", "hw.optional.avx2_0"])[
|
||||
-1
|
||||
]
|
||||
== "1"
|
||||
):
|
||||
return {"AVX2"}
|
||||
elif platform.system() == "Linux":
|
||||
import numpy.distutils.cpuinfo
|
||||
|
||||
result = set()
|
||||
if "avx2" in numpy.distutils.cpuinfo.cpu.info[0].get('flags', ""):
|
||||
if "avx2" in numpy.distutils.cpuinfo.cpu.info[0].get("flags", ""):
|
||||
result.add("AVX2")
|
||||
if "avx512" in numpy.distutils.cpuinfo.cpu.info[0].get('flags', ""):
|
||||
if "avx512" in numpy.distutils.cpuinfo.cpu.info[0].get("flags", ""):
|
||||
result.add("AVX512")
|
||||
if "avx512_fp16" in numpy.distutils.cpuinfo.cpu.info[0].get('flags', ""):
|
||||
if "avx512_fp16" in numpy.distutils.cpuinfo.cpu.info[0].get(
|
||||
"flags", ""
|
||||
):
|
||||
# avx512_fp16 is supported starting SPR
|
||||
result.add("AVX512_SPR")
|
||||
if is_sve_supported():
|
||||
@@ -84,6 +103,7 @@ def supported_instruction_sets():
|
||||
return result
|
||||
return set()
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
instruction_sets = None
|
||||
@@ -93,8 +113,10 @@ opt_env_variable_name = "FAISS_OPT_LEVEL"
|
||||
opt_level = os.environ.get(opt_env_variable_name, None)
|
||||
|
||||
if opt_level is None:
|
||||
logger.debug(f"Environment variable {opt_env_variable_name} is not set, " \
|
||||
"so let's pick the instruction set according to the current CPU")
|
||||
logger.debug(
|
||||
f"Environment variable {opt_env_variable_name} is not set, "
|
||||
"so let's pick the instruction set according to the current CPU"
|
||||
)
|
||||
instruction_sets = supported_instruction_sets()
|
||||
else:
|
||||
logger.debug(f"Using {opt_level} as an instruction set.")
|
||||
@@ -107,10 +129,13 @@ if has_AVX512_SPR:
|
||||
try:
|
||||
logger.info("Loading faiss with AVX512-SPR support.")
|
||||
from .swigfaiss_avx512_spr import *
|
||||
|
||||
logger.info("Successfully loaded faiss with AVX512-SPR support.")
|
||||
loaded = True
|
||||
except ImportError as e:
|
||||
logger.info(f"Could not load library with AVX512-SPR support due to:\n{e!r}")
|
||||
logger.info(
|
||||
f"Could not load library with AVX512-SPR support due to:\n{e!r}"
|
||||
)
|
||||
# reset so that we load without AVX512 below
|
||||
loaded = False
|
||||
|
||||
@@ -119,10 +144,13 @@ if has_AVX512 and not loaded:
|
||||
try:
|
||||
logger.info("Loading faiss with AVX512 support.")
|
||||
from .swigfaiss_avx512 import *
|
||||
|
||||
logger.info("Successfully loaded faiss with AVX512 support.")
|
||||
loaded = True
|
||||
except ImportError as e:
|
||||
logger.info(f"Could not load library with AVX512 support due to:\n{e!r}")
|
||||
logger.info(
|
||||
f"Could not load library with AVX512 support due to:\n{e!r}"
|
||||
)
|
||||
# reset so that we load without AVX512 below
|
||||
loaded = False
|
||||
|
||||
@@ -131,6 +159,7 @@ if has_AVX2 and not loaded:
|
||||
try:
|
||||
logger.info("Loading faiss with AVX2 support.")
|
||||
from .swigfaiss_avx2 import *
|
||||
|
||||
logger.info("Successfully loaded faiss with AVX2 support.")
|
||||
loaded = True
|
||||
except ImportError as e:
|
||||
@@ -143,6 +172,7 @@ if has_SVE and not loaded:
|
||||
try:
|
||||
logger.info("Loading faiss with SVE support.")
|
||||
from .swigfaiss_sve import *
|
||||
|
||||
logger.info("Successfully loaded faiss with SVE support.")
|
||||
loaded = True
|
||||
except ImportError as e:
|
||||
@@ -155,6 +185,7 @@ if not loaded:
|
||||
# we import * so that the symbol X can be accessed as faiss.X
|
||||
logger.info("Loading faiss.")
|
||||
from .swigfaiss import *
|
||||
|
||||
logger.info("Successfully loaded faiss.")
|
||||
except ModuleNotFoundError:
|
||||
formatted_ins_sets = ", ".join(supported_instruction_sets())
|
||||
@@ -165,7 +196,6 @@ if not loaded:
|
||||
f"A) Set the correct FAISS_OPT_LEVEL value when executing "
|
||||
f"'cmake'.\n"
|
||||
f"B) Build the correct SWIG wrapper.\n\n"
|
||||
|
||||
f"These are the supported instruction sets on your system:\n"
|
||||
f"{formatted_ins_sets}\n"
|
||||
f"- If 'AVX512_SPR' (case insensitive) is supported on your "
|
||||
|
||||
@@ -83,7 +83,9 @@ if found_swigfaiss_avx512:
|
||||
if found_swigfaiss_avx512_spr:
|
||||
print(f"Copying {swigfaiss_avx512_spr_lib}")
|
||||
shutil.copyfile("swigfaiss_avx512_spr.py", "faiss/swigfaiss_avx512_spr.py")
|
||||
shutil.copyfile(swigfaiss_avx512_spr_lib, f"faiss/_swigfaiss_avx512_spr{ext}")
|
||||
shutil.copyfile(
|
||||
swigfaiss_avx512_spr_lib, f"faiss/_swigfaiss_avx512_spr{ext}"
|
||||
)
|
||||
|
||||
if found_callbacks:
|
||||
print(f"Copying {callbacks_lib}")
|
||||
@@ -97,7 +99,8 @@ if found_swigfaiss_sve:
|
||||
if found_faiss_example_external_module_lib:
|
||||
print(f"Copying {faiss_example_external_module_lib}")
|
||||
shutil.copyfile(
|
||||
"faiss_example_external_module.py", "faiss/faiss_example_external_module.py"
|
||||
"faiss_example_external_module.py",
|
||||
"faiss/faiss_example_external_module.py",
|
||||
)
|
||||
shutil.copyfile(
|
||||
faiss_example_external_module_lib,
|
||||
|
||||
@@ -45,9 +45,7 @@ def is_perf_counter(key: str) -> bool:
|
||||
|
||||
|
||||
def accumulate_perf_counter(
|
||||
phase: str,
|
||||
t: PerfCounters,
|
||||
counters: Dict[str, int]
|
||||
phase: str, t: PerfCounters, counters: Dict[str, int]
|
||||
):
|
||||
counters[f"{phase}_wall_time_us"] = int(t.wall_time_s * US_IN_S)
|
||||
counters[f"{phase}_user_time_us"] = int(t.user_time_s * US_IN_S)
|
||||
@@ -198,8 +196,8 @@ def main():
|
||||
for counter, values in result.items():
|
||||
if is_perf_counter(counter):
|
||||
print(
|
||||
"%s t=%.3f us (± %.4f)" %
|
||||
(counter, np.mean(values), np.std(values))
|
||||
"%s t=%.3f us (± %.4f)"
|
||||
% (counter, np.mean(values), np.std(values))
|
||||
)
|
||||
|
||||
|
||||
|
||||
+25
-15
@@ -5,7 +5,12 @@
|
||||
|
||||
# a few common functions for the tests
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
from __future__ import (
|
||||
absolute_import,
|
||||
division,
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
@@ -50,8 +55,9 @@ class Randu10k:
|
||||
D, I = DI
|
||||
e = {}
|
||||
for rank in 1, 10, 100:
|
||||
e[rank] = ((I[:, :rank] == self.gt.reshape(-1, 1)).sum() /
|
||||
float(self.nq))
|
||||
e[rank] = (I[:, :rank] == self.gt.reshape(-1, 1)).sum() / float(
|
||||
self.nq
|
||||
)
|
||||
return e
|
||||
|
||||
|
||||
@@ -77,9 +83,9 @@ class Randu10kUnbalanced(Randu10k):
|
||||
|
||||
def get_dataset(d, nb, nt, nq):
|
||||
rs = np.random.RandomState(123)
|
||||
xb = rs.rand(nb, d).astype('float32')
|
||||
xt = rs.rand(nt, d).astype('float32')
|
||||
xq = rs.rand(nq, d).astype('float32')
|
||||
xb = rs.rand(nb, d).astype("float32")
|
||||
xt = rs.rand(nt, d).astype("float32")
|
||||
xq = rs.rand(nq, d).astype("float32")
|
||||
|
||||
return (xt, xb, xq)
|
||||
|
||||
@@ -88,7 +94,7 @@ def get_dataset_2(d, nt, nb, nq):
|
||||
"""A dataset that is not completely random but still challenging to
|
||||
index
|
||||
"""
|
||||
d1 = 10 # intrinsic dimension (more or less)
|
||||
d1 = 10 # intrinsic dimension (more or less)
|
||||
n = nb + nt + nq
|
||||
rs = np.random.RandomState(1338)
|
||||
x = rs.normal(size=(n, d1))
|
||||
@@ -97,14 +103,14 @@ def get_dataset_2(d, nt, nb, nq):
|
||||
# higher factor (>4) -> higher frequency -> less linear
|
||||
x = x * (rs.rand(d) * 4 + 0.1)
|
||||
x = np.sin(x)
|
||||
x = x.astype('float32')
|
||||
return x[:nt], x[nt:nt + nb], x[nt + nb:]
|
||||
x = x.astype("float32")
|
||||
return x[:nt], x[nt : nt + nb], x[nt + nb :]
|
||||
|
||||
|
||||
def make_binary_dataset(d, nt, nb, nq):
|
||||
assert d % 8 == 0
|
||||
rs = np.random.RandomState(123)
|
||||
x = rs.randint(256, size=(nb + nq + nt, int(d / 8))).astype('uint8')
|
||||
x = rs.randint(256, size=(nb + nq + nt, int(d / 8))).astype("uint8")
|
||||
return x[:nt], x[nt:-nq], x[-nq:]
|
||||
|
||||
|
||||
@@ -115,19 +121,22 @@ def compare_binary_result_lists(D1, I1, D2, I2):
|
||||
assert D1.shape == I1.shape == D2.shape == I2.shape
|
||||
n, k = D1.shape
|
||||
ndiff = (D1 != D2).sum()
|
||||
assert ndiff == 0, '%d differences in distance matrix %s' % (
|
||||
ndiff, D1.shape)
|
||||
assert ndiff == 0, "%d differences in distance matrix %s" % (
|
||||
ndiff,
|
||||
D1.shape,
|
||||
)
|
||||
|
||||
def normalize_DI(D, I):
|
||||
norm = I.max() + 1.0
|
||||
Dr = D.astype('float64') + I / norm
|
||||
Dr = D.astype("float64") + I / norm
|
||||
# ignore -1s and elements on last column
|
||||
Dr[I1 == -1] = 1e20
|
||||
Dr[D == D[:, -1:]] = 1e20
|
||||
Dr.sort(axis=1)
|
||||
return Dr
|
||||
|
||||
ndiff = (normalize_DI(D1, I1) != normalize_DI(D2, I2)).sum()
|
||||
assert ndiff == 0, '%d differences in normalized D matrix' % ndiff
|
||||
assert ndiff == 0, "%d differences in normalized D matrix" % ndiff
|
||||
|
||||
|
||||
# Map SIMDLevel enum values to their string names.
|
||||
@@ -218,7 +227,8 @@ def for_all_simd_levels(cls):
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
faiss.SIMDConfig.set_level(
|
||||
faiss.SIMDConfig.get_dispatched_level())
|
||||
faiss.SIMDConfig.get_dispatched_level()
|
||||
)
|
||||
|
||||
Parameterized.__name__ = f"{cls.__name__}_{ln}"
|
||||
Parameterized.__qualname__ = f"{cls.__qualname__}_{ln}"
|
||||
|
||||
@@ -121,9 +121,7 @@ INDEX_BINARY_TYPES = [
|
||||
|
||||
|
||||
def sanitize_index_key(index_key: str) -> str:
|
||||
return (
|
||||
index_key.replace(",", "__").replace("(", "--").replace(")", "--")
|
||||
)
|
||||
return index_key.replace(",", "__").replace("(", "--").replace(")", "--")
|
||||
|
||||
|
||||
def generate_vectors(d: int, nb: int, seed: int) -> np.ndarray:
|
||||
@@ -133,17 +131,13 @@ def generate_vectors(d: int, nb: int, seed: int) -> np.ndarray:
|
||||
return xb
|
||||
|
||||
|
||||
def write_vectors(
|
||||
output_dir: str, vectors: np.ndarray, filename: str
|
||||
) -> None:
|
||||
def write_vectors(output_dir: str, vectors: np.ndarray, filename: str) -> None:
|
||||
path = os.path.join(output_dir, filename)
|
||||
print(f"Writing vectors to: {path}")
|
||||
np.save(path, vectors)
|
||||
|
||||
|
||||
def create_and_populate_index(
|
||||
index_key: str, xb: np.ndarray
|
||||
) -> faiss.Index:
|
||||
def create_and_populate_index(index_key: str, xb: np.ndarray) -> faiss.Index:
|
||||
d = xb.shape[1]
|
||||
index = faiss.index_factory(d, index_key)
|
||||
print(f"Created index type: {type(index).__name__}")
|
||||
@@ -156,9 +150,7 @@ def create_and_populate_index(
|
||||
# IDMap indexes require add_with_ids() instead of add()
|
||||
if "IDMap" in index_key:
|
||||
ids = np.arange(xb.shape[0], dtype=np.int64)
|
||||
print(
|
||||
f"Using add_with_ids() with IDs from 0 to {xb.shape[0] - 1}"
|
||||
)
|
||||
print(f"Using add_with_ids() with IDs from 0 to {xb.shape[0] - 1}")
|
||||
index.add_with_ids(xb, ids)
|
||||
else:
|
||||
index.add(xb)
|
||||
@@ -182,9 +174,7 @@ def create_and_populate_binary_index(
|
||||
# IDMap indexes require add_with_ids() instead of add()
|
||||
if "IDMap" in index_key:
|
||||
ids = np.arange(xb.shape[0], dtype=np.int64)
|
||||
print(
|
||||
f"Using add_with_ids() with IDs from 0 to {xb.shape[0] - 1}"
|
||||
)
|
||||
print(f"Using add_with_ids() with IDs from 0 to {xb.shape[0] - 1}")
|
||||
index.add_with_ids(xb, ids)
|
||||
else:
|
||||
index.add(xb)
|
||||
@@ -193,9 +183,7 @@ def create_and_populate_binary_index(
|
||||
return index
|
||||
|
||||
|
||||
def write_index(
|
||||
index: faiss.Index, output_dir: str, filename: str
|
||||
) -> None:
|
||||
def write_index(index: faiss.Index, output_dir: str, filename: str) -> None:
|
||||
index_path = os.path.join(output_dir, filename)
|
||||
|
||||
# Delete existing file if present
|
||||
@@ -359,9 +347,7 @@ def test_write_binary_index_type(
|
||||
}
|
||||
|
||||
|
||||
def write_test_all_files(
|
||||
writer: str, output_dir: str, seed: int
|
||||
) -> int:
|
||||
def write_test_all_files(writer: str, output_dir: str, seed: int) -> int:
|
||||
print(f"{writer.capitalize()} Writer: Testing index serialization")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
@@ -401,13 +387,12 @@ def write_test_all_files(
|
||||
|
||||
# Now write binary indexes
|
||||
print(
|
||||
f"\n{writer.capitalize()} Writer: "
|
||||
"Testing binary index serialization"
|
||||
f"\n{writer.capitalize()} Writer: " "Testing binary index serialization"
|
||||
)
|
||||
print(f"\nGenerating binary vectors: {nb} database vectors, dimension {d}")
|
||||
assert d % 8 == 0
|
||||
np.random.seed(seed)
|
||||
xb_binary = np.random.randint(256, size=(nb, int(d / 8))).astype('uint8')
|
||||
xb_binary = np.random.randint(256, size=(nb, int(d / 8))).astype("uint8")
|
||||
write_vectors(output_dir, xb_binary, f"vectors_db_binary_{writer}.npy")
|
||||
|
||||
binary_results = []
|
||||
@@ -508,9 +493,7 @@ def read_test_all_files(reader: str, writer: str, input_dir: str) -> int:
|
||||
|
||||
# Now read binary indexes
|
||||
print(f"\n{reader} Reader: Testing binary index deserialization")
|
||||
binary_metadata = read_metadata(
|
||||
input_dir, f"{writer}_binary_metadata.json"
|
||||
)
|
||||
binary_metadata = read_metadata(input_dir, f"{writer}_binary_metadata.json")
|
||||
|
||||
print(f"Binary metadata from {writer} build:")
|
||||
print_metadata(binary_metadata)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user