Wie geht es dir im Whirlpool von Corona? Ich habe faiss mit Go und Rust verwendet, die vor kurzem mit dem Studium begonnen haben, um die zusätzliche Zeit für das Lernen und die Überprüfung aufzuwenden, die ich normalerweise nicht haben kann, weil ich nicht unnötig ausgehen muss. Ich habe beschlossen, es zu versuchen. Faiss ist eine beliebte Nachbarschaftssuchbibliothek von Facebook Resarch, die auch in Einführung der leicht nischenhaften Funktionen von faiss eingeführt wurde.
Dann möchte ich es in der Reihenfolge Python, Go, Rust implementieren.
python Erstens ist die Umweltkonstruktion. Wenn Sie dem Installationshandbuch der ursprünglichen Familie folgen, wird das Modul problemlos installiert. Es wird auch beschrieben, wie man mit "conda" installiert, aber ich persönlich habe eine bittere Erinnerung an die "conda" -Umgebung, also habe ich sie aus dem Quellcode erstellt. Als nächstes kommt der Quellcode. Das gleiche gilt für Go und Rust, aber ich wollte die Leistung später messen, also gebe ich das Protokoll mit json aus. Wenn Sie den Speicher nicht in jeder Iteration freigeben, wird der Speicher mit jeder Iteration weiter vergrößert. Setzen Sie daher die Variablen "del variable" und "gc.collect ()" am Ende, um den Speicher zwangsweise freizugeben. Es ist.
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 Als nächstes geht es los. Dies beginnt auch mit dem Umgebungsbau. Für Go ist das alles! Da ich den Faiss-Wrapper nicht hatte, entschied ich mich für zhyon404 / faiss, was das einfachste Wrapping zu sein scheint. In diesem Repository wird die Umgebung von Docker bereitgestellt. Folgen Sie also der README-Datei und erstellen Sie Docker, um die Umgebung zu erstellen. Es war. Als nächstes kommt der Quellcode. Go verwendet außerdem "logrus.JSONFormatter", um das Protokoll in die JSON-Ausgabe auszugeben, und gibt den Speicher bei jeder Iteration frei. Insbesondere dem Faiss-Index ist im C-Bereich Speicher zugewiesen, sodass er nicht einfach freigegeben werden konnte, und es fiel mir schwer, eine Methode namens "faiss_go.FaissIndexFree" zu finden.
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 Endlich Rust. Auch hier ist es aus dem Umweltbau. Ich habe mich entschieden, Enet4 / faiss-rs, das auch in Docs.rs veröffentlicht wird, als Wrapper für faiss zu verwenden. Grundsätzlich können Sie es installieren, indem Sie [README] folgen (https://github.com/Enet4/faiss-rs#installing-as-a-dependency).
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.
Es ist wichtig, nicht zu vergessen, den Pfad zur Bibliothek hinzuzufügen. Abhängig von der Umgebung scheint es auch nicht zu funktionieren, wenn es nicht zu "LIBRARY_PATH" hinzugefügt wird.
Als nächstes kommt der Quellcode. Dies verwendet auch "json_logger", um das Protokoll an json auszugeben. Ich habe struct
gemäß dem Beispiel definiert, aber ich frage mich, ob es einen besseren Weg gibt. Ich denke. Da die Zufallszahlengenerierung von Rust langsam und die Leistungsmessung schwierig war, verwenden Sie "rand_xorshift" unter Bezugnahme auf Unterschied in der Verarbeitungsgeschwindigkeit beim Generieren von Zufallszahlen mit Rust. Ich habe es gemacht. Interessant war, dass es im Gegensatz zu Python und Go möglich war, es zu implementieren, ohne sich der Speicherfreigabe besonders bewusst zu sein, obwohl die Speicherzuweisung im C-Bereich beteiligt war.
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());
}
}
}
Nun, selbst wenn Sie Python, GO oder Rust verwenden, wird nur die C-API von faiss umschlossen, sodass ich dachte, dass es keinen Unterschied in der Leistung geben würde, aber da ich es implementiert habe, habe ich beschlossen, die Leistung zu messen. .. OpenBLAS wurde für die Matrixberechnungsbibliothek verwendet, m5.large von AWS EC2 wurde für die Umgebung verwendet, und AMI wurde mit "Canonical, Ubuntu, 18.04 LTS, amd64 bionic image build on 2020-01-12" gemessen. Die folgende Grafik zeigt die durchschnittliche Verarbeitungszeit für jede Anzahl von Daten, wobei die Anzahl der Zieldaten für das Suchen und Hinzufügen zwischen 10 ^ 5 $ und 10 ^ 6 $ 100 Mal verschoben wird.
add
search
In allen Sprachen nimmt die Verarbeitungszeit linear mit der Zunahme der Datenanzahl zu. Mit add gab es fast keinen Unterschied in der Leistung zwischen den drei Sprachen, aber mit der Suche war das Ergebnis, dass nur Python eine bessere Leistung hatte. Ich war ein wenig überrascht, weil ich dachte, dass es keinen Unterschied in der Leistung geben würde, weil es sich um ähnliche Wrapper handelt. Wenn Sie außerdem "Go-Verarbeitungszeit / Python-Verarbeitungszeit" zeichnen, um zu sehen, wie sich dieser Leistungsunterschied in Abhängigkeit von der Anzahl der Daten ändert.
Es scheint, dass Go und Rust langsamer sind als Python, mit einem Durchschnitt von 1,44-mal, unabhängig von der Anzahl der Daten.
Ich habe versucht, faiss, eine Nachbarschaftssuchbibliothek von Facebook Research, mit Python, Go und Rust zu verwenden, und die Leistung gemessen. Als ich versuchte, dieselbe Bibliothek in verschiedenen Sprachen zu verwenden, war es interessant, so etwas wie eine Gewohnheit jeder Sprache zu sehen. Insbesondere fand ich es ermutigend, dass Rust sogar den im C-Bereich zugewiesenen Speicher ordnungsgemäß freigibt. Die Sprache hat sich seit der Implementierung in C erheblich weiterentwickelt. In Bezug auf die Leistung sind Go und Rust ungefähr gleich und nur Python ist 1,44-mal schneller. Ich war überrascht anzunehmen, dass die Leistung insofern gleich sein würde, als alle drei Sprachen in C geschriebene Faiss-Wrapper sind. Nur Python wird offiziell unterstützt, und die Kompilierungsmethode unterscheidet sich zwischen Python und Go / Rust. Daher frage ich mich, ob dies eine Rolle spielt. Diesmal wird es interessant sein, dort tief zu graben!
Recommended Posts