Comment vas-tu dans le tourbillon de Corona? J'ai utilisé faiss avec Go and Rust, que j'ai récemment commencé à étudier, afin de consacrer mon temps libre à l'étude et à la vérification que je ne peux habituellement pas utiliser car je n'ai pas à sortir inutilement. J'ai décidé de l'essayer. Faiss est une bibliothèque de recherche de quartier préférée fournie par Facebook Resarch, qui a également été introduite dans Introduction des fonctions légèrement niches de faiss.
Ensuite, je voudrais l'implémenter dans l'ordre de python, Go, Rust.
python
Le premier est la construction de l'environnement. Si vous suivez le Guide d'installation de la famille d'origine, le module sera installé fondamentalement sans aucun problème. Il décrit également comment installer avec conda
, mais j'ai personnellement un souvenir amer de l'environnement conda
, donc je l'ai construit à partir des sources.
Vient ensuite le code source. La même chose est vraie pour Go et Rust, mais je voulais mesurer les performances plus tard, donc j'ai sorti le journal avec json. De plus, si vous ne libérez pas la mémoire à chaque itération, la mémoire continuera d'augmenter à chaque itération, alors mettez la del variable
et` gc.collect () ʻà la fin pour libérer la mémoire de force. Il est.
main.py
import gc
import logging
import sys
from time import time
import faiss
import numpy as np
from pythonjsonlogger import jsonlogger
def elapsed(f, *args, **kwargs):
start = time()
f(*args, **kwargs)
elapsed_time = time() - start
return elapsed_time
if __name__ == '__main__':
# Prepare log.
logger = logging.getLogger()
formatter = jsonlogger.JsonFormatter('(levelname) (asctime) (pathname) (lineno) (message)')
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Define basic information.
d = 512
N = 1e6
nd = 10
nbs = np.arange(N / nd, N + 1, N / nd).astype(int)
nq = 10
k = 5
# Start measuring performance.
for i in range(100):
for nb in nbs:
# Prepare data.
xb = np.random.rand(nb, 512).astype(np.float32)
xq = np.random.rand(nq, 512).astype(np.float32)
# Construct index.
index = faiss.IndexFlatL2(d)
# Evaluate performance.
elapsed_add = elapsed(index.add, xb)
elapsed_search = elapsed(index.search, xq, k)
# Log performance.
logger.info('end one iteration.', extra={
'i': i,
'nb': nb,
'elapsed_add': elapsed_add,
'elapsed_search': elapsed_search
})
# Force to free memory.
del xb
del xq
del index
gc.collect()
Go
Le prochain est Go. Cela commence également par la construction de l'environnement. Pour Go, c'est ça! Je n'avais pas le wrapper faiss, j'ai donc décidé d'utiliser zhyon404 / faiss, qui semble être le wrapping le plus simple. Dans ce référentiel, l'environnement est fourni par Docker, alors suivez le README et faites docker build
pour créer l'environnement. C'était.
Vient ensuite le code source. Go utilise également logrus.JSONFormatter
pour afficher le journal vers la sortie json, et libère également la mémoire à chaque itération. En particulier, l'index faiss est réservé dans la zone C, donc il n'a pas été libéré facilement, et j'ai eu du mal à trouver une méthode appelée faiss_go.FaissIndexFree
.
main.go
package main
import (
"github.com/facebookresearch/faiss/faiss_go"
log "github.com/sirupsen/logrus"
"math/rand"
"os"
"runtime"
"runtime/debug"
"time"
)
func main() {
// Prepare log.
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stdout)
// Define basic information.
d := 512
nbs := []int{1e5, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, 8e5, 9e5, 1e6}
nq := 10
k := 5
// Start measuring performance.
for i := 0; i < 100; i++ {
for _, nb := range nbs {
// Prepare data.
xb := make([]float32, d*nb)
xq := make([]float32, d*nq)
for i := 0; i < nb; i++ {
for j := 0; j < d; j++ {
xb[d*i+j] = rand.Float32()
}
}
for i := 0; i < nq; i++ {
for j := 0; j < d; j++ {
xq[d*i+j] = rand.Float32()
}
}
// Construct index.
v := new(faiss_go.Faissindexflatl2)
faiss_go.FaissIndexflatl2NewWith(&v, d)
index := (*faiss_go.Faissindex)(v)
// Evaluate performance.
add_start := time.Now()
faiss_go.FaissIndexAdd(index, nb, xb)
add_end := time.Now()
I := make([]int, k*nq)
D := make([]float32, k*nq)
search_start := time.Now()
faiss_go.FaissIndexSearch(index, nq, xq, k, D, I)
search_end := time.Now()
// Log performance.
log.WithFields(log.Fields{
"i": i,
"nb": nb,
"elapsed_add": add_end.Sub(add_start).Seconds(),
"elapsed_search": search_end.Sub(search_start).Seconds(),
}).Info("end one iteration.")
// Force to free memory.
faiss_go.FaissIndexFree(index)
runtime.GC()
debug.FreeOSMemory()
}
}
}
Rust Enfin, Rust. Ici aussi, c'est de l'environnement construction. J'ai décidé d'utiliser Enet4 / faiss-rs, qui est également publié dans Docs.rs, comme wrapper pour faiss. En gros, vous pouvez l'installer en suivant README.
This will result in the dynamic library faiss_c ("libfaiss_c.so" in Linux), which needs to be installed in a place where your system will pick up. In Linux, try somewhere in the LD_LIBRARY_PATH environment variable, such as "/usr/lib", or try adding a new path to this variable.
Il est important de ne pas oublier d'ajouter le chemin d'accès à la bibliothèque. De plus, selon l'environnement, cela ne semble pas fonctionner à moins qu'il ne soit ajouté à LIBRARY_PATH
.
Vient ensuite le code source. Cela utilise également json_logger
pour afficher le journal vers json. J'ai défini struct
en fonction de l'exemple, mais je me demande s'il existe un meilleur moyen. Je pense. De plus, étant donné que la génération de nombres aléatoires de Rust était lente et que la mesure des performances était difficile, utilisez rand_xorshift
en vous référant à Différence de vitesse de traitement lors de la génération de nombres aléatoires avec Rust. Je l'ai fait. Ce qui était intéressant, c'est que, contrairement à python et Go, il était possible de l'implémenter sans être particulièrement conscient de la libération de la mémoire, même si cela impliquait de sécuriser la mémoire dans la zone C.
Cargo.toml
[package]
name = "rust"
version = "0.1.0"
authors = []
edition = "2018"
[dependencies]
faiss = "0.8.0"
json_logger = "0.1"
log = "0.4"
rand = "0.7"
rand_xorshift = "0.2"
rustc-serialize = "0.3"
main.rs
use faiss::{Index, index_factory, MetricType};
use log::{info, LevelFilter};
use rand::{RngCore, SeedableRng};
use rand_xorshift::XorShiftRng;
use rustc_serialize::json;
use std::time::Instant;
#[derive(RustcEncodable)]
struct LogMessage<'a> {
msg: &'a str,
i: i32,
nb: i32,
elapsed_add: f32,
elapsed_search: f32
}
fn main() {
// Prepare log.
json_logger::init("faiss", LevelFilter::Info).unwrap();
// Define basic information.
let d: i32 = 512;
const N: i32 = 1_000_000;
let nd: i32 = 10;
let nbs: Vec<i32> = (N/nd..N+1).step_by((N/nd) as usize).collect();
let nq: i32 = 10;
let k: usize = 5;
let mut rng: XorShiftRng = SeedableRng::from_entropy();
// Start measuring performance.
for i in 0..100 {
for &nb in nbs.iter() {
// Prepare data.
let xb: Vec<f32> = (0..nb*d).map(|_| rng.next_u32() as f32 / u32::max_value() as f32).collect();
let xq: Vec<f32> = (0..nq*d).map(|_| rng.next_u32() as f32 / u32::max_value() as f32).collect();
// Construct index.
let mut index = index_factory(d as u32, "Flat", MetricType::L2).unwrap();
// Evaluate performance.
let start = Instant::now();
index.add(&xb).unwrap();
let elapsed_add = start.elapsed().as_micros() as f32 / 1e6;
let start = Instant::now();
index.search(&xq, k).unwrap();
let elapsed_search = start.elapsed().as_micros() as f32 / 1e6;
// Log performance.
info!("{}", json::encode(&LogMessage {
msg: "end one iteration.", i, nb, elapsed_add, elapsed_search
}).unwrap());
}
}
}
Eh bien, même si vous prenez python, GO ou Rust, cela n'enveloppe que l'API C de faiss, donc j'ai pensé qu'il n'y aurait pas de différence de performances, mais depuis que je l'ai implémenté, j'ai décidé de mesurer les performances. .. OpenBLAS a été utilisé pour la bibliothèque de calcul matriciel, m5.large d'AWS EC2 a été utilisé pour l'environnement et l'AMI a été mesurée avec Canonical, Ubuntu, 18.04 LTS, amd64 bionic image build on 2020-01-12
.
Le graphique ci-dessous montre le temps de traitement moyen pour chaque nombre de données, avec le nombre de données cibles déplacées entre 10 $ ^ 5 $ et 10 $ ^ 6 $ 100 fois pour la recherche et l'ajout.
add
search
Dans toutes les langues, le temps de traitement augmente linéairement avec l'augmentation du nombre de données. Avec l'ajout, il n'y avait presque aucune différence de performances entre les trois langages, mais avec la recherche, le résultat était que seul python avait de meilleures performances. J'ai été un peu surpris car je pensais qu'il n'y aurait pas de différence de performance car ce sont des emballages similaires. De plus, si vous tracez Go temps de traitement / temps de traitement python
pour voir comment cette différence de performance change en fonction du nombre de données.
Il semble que Go et Rust soient plus lents que python, avec une moyenne de 1,44 fois quel que soit le nombre de données.
J'ai essayé d'utiliser faiss, qui est une bibliothèque de recherche de quartier de Facebook Research, avec python, Go et Rust, et j'ai mesuré les performances. Quand j'ai essayé d'utiliser la même bibliothèque dans différentes langues, c'était intéressant de voir quelque chose comme une habitude de chaque langue. En particulier, j'ai senti qu'il était encourageant que Rust libère même la mémoire allouée dans la zone C correctement. Le langage a considérablement évolué depuis l'époque où il a été implémenté dans C. En termes de performances, Go et Rust sont à peu près les mêmes, et seul python est 1,44 fois plus rapide. J'ai été surpris de supposer que la performance serait la même dans la mesure où les trois langues sont des wrappers faiss écrits en C. Seul python est officiellement pris en charge, et la méthode de compilation est différente entre python et Go / Rust, je me demande donc si cela est impliqué. Il sera intéressant de creuser profondément là-bas cette fois!
Recommended Posts