Bash-Skripte im Schlangengriff
Mit Python- statt Bash-Skripten Programme aufrufen und Ergebisse auswerten
Aus Python heraus ruft man auch ohne große Mühe Programme auf, die in anderen Sprachen geschrieben wurden. Für typische Programmieraufgaben brauchen Python-Programme aber wesentlich weniger Code als Bash-Skripte. Daher eignet sich Python, um mit eleganterem Code Bash-Skripte zu ersetzen.
Zum Testen neuer Nvidia-Grafikkarten (siehe S. 92) führen wir in der c’t-Redaktion unter anderem den Machine-Learning-Benchmark DeepBench aus. Der in C programmierte Benchmark besteht aus drei ausführbaren Einzeltests, die je zwei Parameter akzeptieren und jeweils eine ganze Batterie an Zeitmessungen ausspucken, beispielsweise für Matrixmultiplikationen mit verschieden großen Matrizen. Um verfälschte Ergebnisse durch zufällig dazwischengrätschende Systemprozesse zu vermeiden, führen wir jeden Benchmark dreimal aus und verwenden von jedem Einzelwert das beste Ergebnis. Diese Ergebnisse dampfen wir mit dem geometrischen Mittel auf einen einzelnen Wert pro Test ein, um ein Gesamtergebnis für die Performance einer Grafikkarte bei dieser Art von Berechnung zu erhalten. Man könnte die dafür nötigen 45 Tests per Hand starten, die Ergebnisse in Excel kopieren und dort auswerten. Mit einem Python-Skript macht der Testrechner das aber automatisch. Unser Beispiel zeigt, wie leicht Python Bash-Skripte und Batch-Dateien ersetzt.
Ein Bash-Skript könnte mit ein paar Schleifen relativ leicht die 45 Einzeltests starten. Beim Auswerten der Ergebnisse müsste man aber einige recht magische sed-Befehle erfinden, um aus den Konsolenausgaben der Benchmarks die Zahlenwerte der Ergebnisse herauszupicken. Spätestens beim Berechnen des geometrischen Mittels wird man sich eine Bibliothek wie NumPy wünschen, die es in so angenehmer Form nicht für die Bash gibt.
Python führt die Tests mit ähnlich wenig Code wie ein Bash-Skript aus, extrahiert danach aber relativ leicht mit regulären Ausdrücken die Ergebnisse. Möglich machen das die Module subprocess, re und numpy. Das erste ruft beliebige Programme als eigene Prozesse auf. Die anderen extrahieren und verarbeiten die Daten.
Mit subprocess.Popen() starten Sie aus Python-Skripten alle Programme, die Sie auch in der Konsole starten können. Als Parameter erwartet der Befehl eine Liste. Die enthält als ersten Eintrag den Namen des Programms und als weitere Einträge die Parameter. Aus ../DeepBench/code/bin/conv_bench train float wird dann:
prc = subprocess.Popen([
"../DeepBench/code/bin/conv_bench",
"train", "float"])
Gibt man Popen() noch den Parameter stdout=subprocess.PIPE mit, leitet der Prozess seine Ausgabe in eine Pipe um. An die kommt man mit communicate():
out = prc.communicate()[0]
In out stehen danach sämtliche Ausgaben, die das aufgerufene Programm normalerweise auf die Konsole geschrieben hätte.
Ausgaben auswerten
DeepBench gibt seine Ergebnisse als mit Leerzeichen formatierte Tabelle aus – ein zum Weiterverarbeiten ungünstiges Format. Aber immerhin sieht die Ausgabe immer gleich aus. Deswegen konnten wir die Ergebnisse mit regulären Ausdrücken filtern. Das übernimmt Pythons re-Modul. Unser regulärer Ausdruck erwartet Ganzzahlen \d+, die durch mindestens ein Leerzeichen \s+ getrennt werden. Da wir die Zahlen weiterverarbeiten möchten, definieren wir eine Capturing-Group, indem wir die Zahlen in runde Klammern einpacken (\d+). re.compile() erzeugt aus dem String ein Regular-Expression-Objekt. Führt man mit dem die Methode match() aus und übergibt je eine Zeile der Benchmark-Ausgaben, entsteht dabei ein Match-Objekt. An die vom regulären Ausdruck definierten Gruppen (das sind die Teile des Ausdrucks, die in runden Klammern stehen) kommt man mit match.groups(). Etwas verkürzt sieht der Code dafür so aus:
res_line = re.compile(r"^\s*(\d+)\s+(\d+)\s+(\w+)\s*$")
for line in out.splitlines():
match = res_line.match(line)
if match:
p, fw_time, f_alg = match.groups()
Matrixrechnen
Wir haben unsere per re extrahierten Benchmarkergebnisse einfach in eine Liste results geschrieben. Jeder Eintrag der Liste besteht seinerseits aus einer Liste aus drei Werten, nämlich dem Ergebnis je eines Benchmarkdurchlaufs. Aus der Listen-Liste macht np.array(results) eine Matrix. Mit der kann NumPy nun bequem rechnen. Beispielsweise sucht min(axis=0) den kleinsten Wert in jeder Spalte der Matrix, sodass dabei ein Vektor entsteht:
np.array(results).min(axis=0)
Danach stehen in diesem Vektor die Laufzeiten des besten aus drei Benchmarkdurchläufen für jeden Einzeltest.
Nun wollten wir aber lieber einen einzelnen Wert statt eines ganzen Vektors angeben. Diesen Wert berechnen wir als geometrisches Mittel über die Ergebnisse der Einzeltests beziehungsweise Zahlen im Vektor. Eine Funktion zum Berechnen des geometrischen Mittels findet sich im Modul scipy.stats unter dem Namen gmean. SciPy stammt von den gleichen Entwicklern wie NumPy und enthält Hunderte von Funktionen für wissenschaftliche Berechnungen. Die Berechnung des Gesamtergebnisses passt so in eine Zeile:
gmean(np.array(results).min(axis=0))
Nie wieder Handarbeit
Python statt Bash lohnt sich gerade dann, wenn man nicht nur mit subprocess.Popen() Programme aufruft, sondern deren Ausgaben auch weiterverarbeiten will. Diese Berechnungen definiert man in Python in wesentlich weniger Zeilen als auf der Konsole.
Das gesamte Beispiel zum Auswerten von DeepBench finden Sie unter ct.de/yzyq. Es enthält einige zusätzliche Codezeilen, um verschieden formatierte Ausgaben der einzelnen Tests jeweils korrekt auszuwerten. Alle wesentlichen Ideen des Skripts haben Sie aber soeben kennengelernt. (pmk@ct.de)
Quellcode:ct.de/yzyq