J'ai essayé d'exécuter faiss avec python, Go, Rust

introduction

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.

la mise en oeuvre

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());
        }
    }
}

Vérification des performances

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 add result

search Screenshot_2020-04-07 Analyze Performance(2).png

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.

performance ratio

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.

Résumé

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

J'ai essayé d'exécuter faiss avec python, Go, Rust
J'ai essayé Grumpy (allez exécuter Python).
J'ai essayé d'exécuter prolog avec python 3.8.2.
J'ai essayé d'exécuter Deep Floor Plan avec Python 3.6.10.
J'ai essayé fp-growth avec python
J'ai essayé gRPC avec Python
J'ai essayé de gratter avec du python
J'ai essayé webScraping avec python.
J'ai essayé la communication SMTP avec Python
J'ai essayé d'exécuter Movidius NCS avec python de Raspberry Pi3
Python avec Go
J'ai essayé le rendu non réaliste avec Python + opencv
J'ai essayé un langage fonctionnel avec Python
J'ai essayé la récurrence avec Python ② (séquence de nombres Fibonatch)
# J'ai essayé quelque chose comme Vlookup avec Python # 2
J'ai essayé de "lisser" l'image avec Python + OpenCV
J'ai essayé des centaines de millions de SQLite avec python
J'ai essayé d'exécuter pymc
J'ai essayé de "différencier" l'image avec Python + OpenCV
J'ai essayé Python> autopep8
J'ai essayé la différenciation jacobienne et partielle avec python
J'ai essayé d'obtenir des données CloudWatch avec Python
J'ai essayé d'utiliser mecab avec python2.7, ruby2.3, php7
J'ai essayé la synthèse de fonctions et le curry avec python
J'ai essayé de sortir LLVM IR avec Python
J'ai essayé de "binariser" l'image avec Python + OpenCV
J'ai essayé d'automatiser la fabrication des sushis avec python
J'ai essayé d'exécuter python -m summpy.server -h 127.0.0.1 -p 8080
J'ai essayé d'exécuter alembic, un outil de migration pour Python
J'ai essayé d'envoyer un email avec SendGrid + Python
J'ai essayé Python> décorateur
J'ai essayé d'exécuter TensorFlow
J'ai essayé LeetCode tous les jours 7. Integer Integer (Python, Go)
J'ai essayé d'exécuter BERT avec Sakura VPS (sans GPU)
J'ai essayé de démarrer avec le script python de blender_Part 01
J'ai essayé de toucher un fichier CSV avec Python
[OpenCV / Python] J'ai essayé l'analyse d'image de cellules avec OpenCV
J'ai essayé d'exécuter python à partir d'un fichier chauve-souris
J'ai essayé de démarrer avec le script python de blender_Partie 02
J'ai essayé d'implémenter le perceptron artificiel avec python
J'ai essayé LeetCode tous les jours 20. Parenthèses valides (Python, Go)
Mayungo's Python Learning Episode 1: J'ai essayé d'imprimer avec impression
[Python] J'ai essayé d'exécuter un serveur local en utilisant flask
J'ai essayé de résoudre le problème avec Python Vol.1
J'ai essayé LeetCode tous les jours 9. Palindrome Number (Python, Go)
J'ai essayé LeetCode tous les jours 1. Two Sum (Python, Go)
J'ai essayé la "conversion de morphologie" de l'image avec Python + OpenCV
J'ai essayé de frapper l'API avec le client python d'echonest
J'ai essayé de résoudre la théorie des nombres entiers d'AOJ avec Python
J'ai essayé Learning-to-Rank avec Elasticsearch!