SPERLI-Wire-Protokoll, Version 1

SPERLI (Simple Parallel Execution over Remote Links Interface) ist die Cluster-Schicht von Hilde: ein Job aus size Rängen, jeder ein Prozess auf einem gelisteten Host, die über TLS miteinander sprechen. SPERLI ist nicht MPI-kompatibel. Es übernimmt nur die Semantik von MPI: Rang, Größe, Senden und Empfangen mit Tag, Barrier, Broadcast, Reduce, Allreduce, Gather und Scatter. Dieses Dokument und die Vektoren in docs/sperli-vectors.txt sind vollständig genug, um eine zweite Implementierung zu schreiben, etwa in reinem C. Englische Fassung: das Sperli-Protokoll.

Umfang und Stand

SPERLI ist der eigene MPI-Ersatz für die Rechenverteilung, in reinem C. Es kommt nach dem Array-Typ und der Rechenschicht von Hilde; nichts, was vom Array-Typ abhängt, wird vorausgebaut.

Alle Ganzzahlen sind Little Endian und ohne Vorzeichen, sofern nichts anderes steht. Eine Bytefolge von n Bytes heißt [n]. "Muss" heißt: Der Empfänger weist den Frame ab, schließt die Verbindung und bricht den Job ab, es sei denn, eine Regel sagt "verwirft".

Transport und Sicherheit

Jobbeschreibung

Der Launcher sagt jedem Rang seinen Job auf der Standardeingabe des Prozesses, als eine Zeile aus kleinen Hexadezimalziffern und einem Zeilenvorschub. Die Bytes hinter den Ziffern sind:

FeldGrößeBedeutung
magic4SPJD
version21
reserviert20
Job-Id16die Job-Id
size4die Zahl der Ränge, 1 bis 4096
rank4der Rang dieses Prozesses, kleiner als size
incarnation41 oder mehr
Hostssize malPort (2 Bytes, nicht 0), Länge des Hostnamens (2 Bytes), Hostname
öffentlicher Schlüssel2 + 1344Länge und der öffentliche CompositeSig-Schlüssel des Jobs
Token2 + tLänge und das Join-Token dieses Rangs
CA4 + nLänge und der PEM-Text des CA-Zertifikats des Jobs
Zertifikat4 + nLänge und der PEM-Text des Zertifikats dieses Rangs
Schlüssel4 + nLänge und der PEM-Text des Ed25519-Schlüssels dieses Rangs

Zahlen sind little endian. Eine Beschreibung mit weiteren Bytes hinter dem Schlüssel, ein abgeschnittenes Feld, ein abgewiesener Host oder eine Größe, ein Rang oder eine Inkarnation außerhalb des Bereichs wird abgewiesen. Der Schlüssel des Rangs ist ein Geheimnis; der Launcher schreibt die Zeile nie in eine Datei, und ein Rang schreibt die drei PEM-Texte in Dateien, die nur sein Benutzer lesen darf, in ein eigenes Verzeichnis, solange er Verbindungen aufbaut.

Verbindungen

Der Rang mit der kleineren Nummer verbindet sich mit dem größeren. Ein Rang hört zuerst an seinem Port, verbindet sich mit jedem höheren Rang (alle 100 ms ein neuer Versuch, 30 s lang, weil der Peer vielleicht noch nicht hört) und nimmt dann von jedem niedrigeren Rang eine Verbindung an. Beide Seiten schicken ihr HELLO sofort. Ein Rang darf höchstens 5000 ms schweigen, bevor er als verloren gilt; ein Rang, der auf einen Frame wartet, schickt in Version 1 nichts von sich aus, und ein Programm, das länger als die Grenze rechnet, hebt sie mit der Stille-Einstellung der Bibliothek an.

Frame

Jede Nachricht ist ein Frame: ein Kopf von 52 Bytes und length Bytes Nutzlast.

OffsetGrößeFeldBedeutung
04lengthBytes der Nutzlast nach dem Kopf
41typeFrame-Typ, siehe unten
51flagsin Version 1 gleich 0; andere Werte muss man abweisen
62service0 = SPERLI; 1 = reserviert für einen gemeinsamen Raft-Transport; andere abweisen
816job id128-Bit-Job-Id, für alle Frames eines Jobs gleich
244incarnation1 beim Start, bei jedem Neustart plus 1
284membership epochin Version 1 gleich 0; reserviert für dynamische Mitgliedschaft; andere abweisen
328epochCheckpoint-Epoche des Frames; 0 für Frames außerhalb eines Checkpoints
404source rankRang des Senders
444target rankRang des Empfängers
484tagNutzer-Tag bei DATA, 0 bei allen anderen Typen

Frame-Typen

CodeNameRichtungNutzlast
1HELLObeide, erster FrameFähigkeiten und Join-Token
2DATAbeliebigNachrichtenbytes
3COLLECTIVEbeliebigKollektiv-Kopf und Daten
4HEARTBEATbeideZähler des Senders
5ABORTbeliebigGrundcode und Text
6CKPT_BEGINRang 0 an alleCheckpoint-Beginn
7CKPT_DATARang an Buddy, oder AbrufStück einer Rang-Datei
8CKPT_ACKRang an Rang 0Digest der geschriebenen Datei
9CKPT_COMMITRang 0 an alledie COMMIT-Datei
10RESUMERang 0 an alle, und AntwortenWiederaufnahme-Epoche und Status
11BYEbeideleer

Code 0 und Codes über 11 sind ungültig.

HELLO (1)

Beide Seiten senden es unmittelbar nach dem TLS-Handshake; davor darf nichts kommen. Ein zweites HELLO wird abgewiesen.

OffsetGrößeFeld
02Protokollversion, 1
21Sprache: 0 Hilde, 1 Go, 2 C
31reserviert, 0
44Dienste als Bitmenge: Bit 0 Punkt-zu-Punkt und Kollektive, Bit 1 Checkpoint, Bit 2 reserviert für Raft
84reserviert, 0
124Länge t des Tokens
16tJoin-Token

Der Kopf eines HELLO trägt Rang und Inkarnation des Senders. Der Peer braucht Bit 0 gemeinsam; eine andere Protokollversion als 1 wird abgewiesen.

Join-Token. Die signierte Nachricht ist

OffsetGrößeFeld
04Magic SPJT
42Token-Version, 1
62reserviert, 0
816Job-Id
244Rang
284Größe (size)
324Inkarnation
368Ablauf, Sekunden seit 1970-01-01 UTC
442Länge h des Hosts, 1 bis 255
46hHostname, ASCII

und das Token ist diese Nachricht, dann eine Signaturlänge von 2 Bytes (2484) und die Signatur. Der Empfänger prüft die Signatur mit dem öffentlichen Job-Schlüssel und weist das Token ab, wenn die Signatur nicht stimmt, die Job-Id nicht die seines Jobs ist, der Rang nicht der Rang im Frame-Kopf und im Zertifikat ist, size oder Inkarnation vom Job abweichen, der Host vom Hostlisten-Eintrag dieses Rangs abweicht oder der Ablauf nicht nach der Uhr des Empfängers liegt.

DATA (2)

Die Nutzlast ist die Nachricht. Der Tag steht im Kopf. Eine leere Nachricht ist gültig.

COLLECTIVE (3)

Nutzlast: ein Kollektiv-Kopf von 16 Bytes, dann Daten. Der tag im Frame-Kopf ist 0, und eine Nutzlast, die kürzer als der Kollektiv-Kopf ist, wird mit dem Frame-Kopf abgewiesen (bevor die Nutzlast gelesen wird). Die Daten haben höchstens 16777200 Bytes, damit Kopf und Daten in die größte Nutzlast passen.

OffsetGrößeFeld
01Operation: 1 barrier, 2 bcast, 3 reduce, 4 allreduce, 5 gather, 6 scatter
11Elementtyp: 0 Bytes, 1 int32, 2 int64, 3 uint32, 4 uint64, 5 float32, 6 float64
21Reduktion: 0 keine, 1 sum, 2 min, 3 max, 4 prod, 5 land, 6 lor, 7 lxor, 8 band, 9 bor, 10 bxor
31Phase: 0 Beitrag zur Wurzel, 1 Ergebnis von der Wurzel
44Wurzelrang
88Folgenummer des Kollektiv-Aufrufs, ab 1; für denselben Aufruf auf allen Rängen gleich
16nDaten

Bytes, barrier, bcast, gather und scatter tragen Elementtyp 0 und Reduktion 0. Reduce und allreduce tragen Typ und Operation des Aufrufs. Jeder Rang macht dieselben Kollektiv-Aufrufe in derselben Reihenfolge; die Folgenummer zählt die Aufrufe eines Rangs ab 1, und ein Aufruf, der scheitert, zählt trotzdem.

Form. Version 1 benutzt einen Stern, keinen Baum und keinen Ring, damit das Ergebnis nicht vom Zeitablauf abhängt und jeder Rang schon mit jedem anderen verbunden ist. Bei allreduce ist die Wurzel Rang 0. Reduziert wird über die Beiträge in aufsteigender Rangfolge, und der eigene Wert der Wurzel ist der Beitrag ihres Rangs. Ein Rang sendet nur an die Wurzel und empfängt nur von der Wurzel; die Wurzel spricht mit jedem Rang.

OperationBeiträge (Phase 0)Ergebnisse (Phase 1)
barrierleer, jeder Rang an Rang 0leer, Rang 0 an jeden Rang
bcastkeinedie Daten der Wurzel an jeden anderen Rang
reduceDaten jedes anderen Rangs an die Wurzelkeine; jeder Rang außer der Wurzel bekommt die leere Zeichenkette
allreduceDaten jedes Rangs an Rang 0die reduzierten Daten, Rang 0 an jeden anderen Rang
gatherder Block jedes anderen Rangs an die Wurzelkeine; die Wurzel fügt die Blöcke in Rangfolge zusammen, die anderen bekommen die leere Zeichenkette
scatterkeineBlock i der Daten der Wurzel an Rang i; die Wurzel behält ihren eigenen Block

Reduktionen. Die Elemente eines Arrays haben je width Bytes (1, 4 oder 8, nach Typ), niederwertigstes Byte zuerst. Zwei Arrays werden Element für Element reduziert, das erste als linker Operand.

OperationElementtypenErgebnis
sum, prodint32, int64, uint32, uint64das Ergebnis modulo 2 hoch Breite in Bits; nichts bricht ab
sum, prodfloat32, float64das IEEE-Ergebnis in der Breite des Typs
min, maxganze Zahlennach der Ordnung des Typs: mit Vorzeichen für int32 und int64, sonst ohne
min, maxFließkommazahlenmin nimmt den zweiten Operanden nur, wenn er kleiner als der erste ist, max nur, wenn er größer ist
land, lor, lxorganze Zahlenjedes Element ist wahr, wenn es nicht 0 ist; das Ergebnis ist 0 oder 1
band, bor, bxorganze Zahlen und Bytesdie Bits

Nicht erlaubt und von jedem Rang abgewiesen, bevor etwas gesendet wird (H1618): sum, min, max, prod und die logischen Operationen auf Bytes; die logischen und bitweisen Operationen auf Fließkommazahlen; Reduktion 0; ein Typ über 6; eine Operation über 10.

Prüfung der Argumente. Jede Prüfung, die jeder Rang allein machen kann, geschieht, bevor etwas gesendet wird, damit alle Ränge denselben Aufruf abweisen und der Job weiterläuft: eine Wurzel ab size, ein Typ oder eine Operation, die nicht zusammenpassen, und Daten, die keine ganze Zahl von Elementen sind (H1618). Prüfungen, die nur ein Rang machen kann, beenden den Job:

Reihenfolge mit DATA. Kollektiv-Frames und DATA-Frames einer Verbindung kommen in der gesendeten Reihenfolge an. Ein Rang, der auf einen Kollektiv-Frame wartet, hebt die DATA-Frames davor für sein eigenes recv auf, und ein Rang, der auf DATA wartet, hebt die Kollektiv-Frames davor auf. Die Tags ab 2147483648 sind dafür reserviert: ein DATA-Frame mit so einem Tag wird mit dem Frame-Kopf abgewiesen (H1610), ein Peer kann also keinen Daten-Frame als Kollektiv-Frame ausgeben.

HEARTBEAT (4)

Nutzlast: 8 Bytes, ein Zähler des Senders, der bei 1 beginnt und je Heartbeat um eins wächst. Jede Seite sendet auf jeder Verbindung alle 1000 ms einen, wenn sie sonst nichts gesendet hat. Eine Verbindung, die 5000 ms keinen Frame geliefert hat, gilt als verloren: Der Rang sendet ABORT mit Grund 1 an alle Peers und hält an.

ABORT (5)

OffsetGrößeFeld
04Grund: 1 Heartbeat verloren, 2 Rang ausgefallen, 3 Protokollfehler, 4 Abbruch durch den Nutzer, 5 Checkpoint fehlgeschlagen
4nText, UTF-8, höchstens 1024 Bytes

Ein Rang, der ABORT empfängt, leitet nichts weiter, schließt alle Verbindungen und beendet den Jobschritt. Mit Checkpoint startet der Launcher den Job vom letzten COMMIT neu (siehe Checkpoints).

BYE (11)

Leere Nutzlast. Ein Rang sendet BYE an jeden Peer, wenn er normal geht, und schließt dann die Verbindung. Ein Schließen ohne BYE ist ein verlorener Peer.

CKPT_BEGIN (6)

Rang 0 an jeden Rang, epoch im Kopf ist die neue Epoche. Ein Rang weist eine andere Version als 1 und eine andere size als seine eigene ab.

OffsetGrößeFeld
04Checkpoint-Formatversion, 1
44size

CKPT_DATA (7)

OffsetGrößeFeld
01Art: 0 Buddy-Kopie, 1 Abrufanfrage, 2 Abrufantwort, 3 Abruf abgelehnt, 4 nichts zu holen
13reserviert, 0
44Eigentümer-Rang, der Rang, dem die Datei gehört
88Offset des Stücks in der Datei
168Gesamtlänge der Datei
24nStück

Art 0: Rang r sendet seine fertige Rang-Datei in Stücken der Reihe nach an Rang (r + 1) mod size, der sie als rank-<r>.ckpt.buddy in seinem eigenen Epochenverzeichnis ablegt. Art 1: Ein Rang fragt den Buddy nach einer Datei, die ihm fehlt; Offset und Gesamtlänge sind 0, es gibt kein Stück. Art 2 antwortet mit den Stücken. Art 3 heißt: Der Buddy hat keine gültige Kopie. Art 4 sagt, dass ein Rang nichts braucht, damit der Buddy nicht auf eine Anfrage wartet. Ein Stück hat höchstens 1 MiB; die Stücke kommen der Reihe nach, der Offset jedes Stücks ist die Zahl der bisher empfangenen Bytes, und die Gesamtlänge ändert sich nie.

CKPT_ACK (8)

Nutzlast: 32 Bytes, das SHA-256 der ganzen Rang-Datei, die eben an ihren Platz umbenannt wurde. epoch im Kopf ist die Epoche.

CKPT_COMMIT (9)

Nutzlast: die Bytes der COMMIT-Datei der Epoche. Empfänger prüfen die Signatur und legen eine Kopie ab.

RESUME (10)

OffsetGrößeFeld
04size
44Status: 0 Anfrage oder bereit, 1 Datei vom Buddy geholt, 2 fehlgeschlagen
832SHA-256 der COMMIT-Datei, 32 Nullbytes für keine
40ndie COMMIT-Datei, bei Status 0 von Rang 0 und bei Status 3

Status 3 ist ein Angebot: Jeder Rang außer 0 sendet Rang 0 die beste COMMIT-Datei, die er hat (epoch im Kopf = ihre Epoche, leer, wenn er keine hat). Rang 0 nimmt unter den Angeboten und seinen eigenen Dateien die höchste Epoche, deren Signatur stimmt, und sendet Status 0 mit dieser Epoche (epoch im Kopf) und der COMMIT-Datei. Jeder Rang antwortet Rang 0 mit 0 oder 1, sobald sein Zustand geladen ist, oder mit 2, wenn er ihn nicht laden kann; Rang 0 sendet dann allen Status 0 zum Weitermachen; Status 2 bricht den Job ab.

Checkpoints

Aufbau auf der Platte: <dir>/<job>/<epoch>/rank-<n>.ckpt und <dir>/<job>/<epoch>/COMMIT. <job> ist die Job-Id als 32 kleine Hex-Ziffern; <epoch> ist eine Dezimalzahl, die bei 1 beginnt.

Rang-Datei. Ein Kopf von 80 Bytes, dann der Zustand.

OffsetGrößeFeld
04Magic SPCK
42Formatversion, 1
62reserviert, 0
816Job-Id
248Epoche
324Rang
364size
408Länge der Nutzlast
4832SHA-256 der Nutzlast
80nNutzlast (der serialisierte Zustand)

Ein Leser weist eine Datei ab mit anderem Magic oder anderer Version, mit Job-Id, Epoche, Rang oder size, die nicht zu dem passen, was er erwartet, mit einer Dateilänge, die nicht 80 plus Nutzlastlänge ist, oder deren Nutzlast nicht zum gespeicherten SHA-256 passt.

COMMIT-Datei.

OffsetGrößeFeld
04Magic SPCM
42Formatversion, 1
62reserviert, 0
816Job-Id
248Epoche
324size
3632 * sizeSHA-256 der ganzen Rang-Datei von Rang 0, 1, ...
36 + 32 * size2Signaturlänge (2484)
38 + 32 * size2484CompositeSig-Signatur über die Bytes 0 bis 36 + 32 * size

Eine Epoche ohne COMMIT, dessen Signatur gegen den öffentlichen Job-Schlüssel stimmt, dessen Job-Id, Epoche und size passen und dessen Hashes zu den Rang-Dateien passen, zählt nicht.

Ablauf. Für das Kollektiv checkpoint(state), ohne offenes Send- oder Receive-Handle auf irgendeinem Rang:

  1. Rang 0 sendet CKPT_BEGIN mit der neuen Epoche e an alle Ränge.
  2. Jeder Rang serialisiert seinen Zustand und schreibt die Rang-Datei unter einem temporären Namen in das Epochenverzeichnis, ruft fsync auf, benennt sie in rank-<n>.ckpt um und sendet CKPT_ACK mit ihrem SHA-256 an Rang 0.
  3. Jeder Rang sendet die Datei als CKPT_DATA Art 0 an seinen Buddy (n + 1) mod size.
  4. Hat Rang 0 alle ACKs, schreibt er COMMIT auf dieselbe Weise (temporärer Name, fsync, Umbenennen), mit dem Job-Schlüssel signiert, und sendet CKPT_COMMIT an alle Ränge. Jeder Rang prüft die Signatur, prüft, dass das COMMIT das SHA-256 seiner eigenen Datei und der Buddy-Kopie, die er hält, aufführt, und legt eine Kopie ab. Ein Rang verlässt checkpoint, wenn er das COMMIT hat. Rang 0 entfernt ein altes COMMIT derselben Epoche, bevor er CKPT_BEGIN sendet, sodass eine halbfertige Epoche nie ein COMMIT eines früheren Versuchs zeigt.
  5. Nur die Epochen e und e - 1 bleiben. Nach dem COMMIT von e wird jedes ältere Verzeichnis gelöscht, ob committet oder nicht. Verzeichnisse einer späteren Epoche bleiben unberührt, weil ein Rang, der weiter ist, sie

schreiben kann.

Ein Checkpoint mit noch offenem Send oder Receive scheitert auf jedem Rang mit H1624; das wird zur Laufzeit geprüft. Der Zustand geht durch die Typklasse Checkpointable; ein Typ, der ein Handle, eine Verbindung oder eine Datei hält, hat keine Instanz, sodass der Compiler den Aufruf abweist. Ein gescheiterter Checkpoint sendet ABORT mit Grund 5 an die anderen.

Ausfall und Wiederaufnahme. Bei Heartbeat-Verlust brechen alle Ränge ab. Der Launcher startet den Job mit Inkarnation plus 1 neu, höchstens --restart-max Mal; ein ausgefallener Rang darf auf einen anderen Host der Liste gehen, nie auf einen gesperrten Host (oder einen Host der Sperrdatei). Die Wiederaufnahme-Epoche ist die höchste Epoche mit gültigem COMMIT. Bei der Wiederaufnahme wird die Signatur der COMMIT-Datei gegen den öffentlichen Job-Schlüssel geprüft, und das SHA-256 jeder Rang-Datei wird zweimal geprüft: gegen den Kopf der Datei und gegen den Eintrag der COMMIT-Datei. Ein Rang, dessen Datei fehlt oder ungültig ist, holt sie mit CKPT_DATA Art 1 von seinem Buddy; die geholte Datei wird ebenso geprüft. Ein Fehler wird mit einem Code abgewiesen: H1619 für eine Datei, die nicht die erwartete ist, H1620 für ein SHA-256, das nicht passt, H1621 für ein COMMIT, das missgebildet ist oder dessen Signatur nicht stimmt, H1622, wenn keine Epoche wieder aufgenommen werden kann. Version 1 verlangt bei der Wiederaufnahme dasselbe size; ein signiertes COMMIT mit anderer size wird mit H1623 abgewiesen. Hat die neueste Epoche kein gültiges COMMIT, eine ältere aber schon, wird die ältere wieder aufgenommen; scheitert ein COMMIT und gilt kein anderes, wird der Code dieses Fehlers gegeben.

Grenzen und reservierte Felder in Version 1

Fest: Mitgliedschaft von size Rängen; service 0; membership epoch 0; flags 0. Sie sind für einen gemeinsamen Raft-und-SPERLI-Transport und für dynamische Mitgliedschaft reserviert und werden noch nicht gedeutet. Ein Empfänger der Version 1 weist Werte ungleich 0 ab; eine spätere Version muss sich daher zuerst im HELLO ankündigen.

Testvektoren

docs/sperli-vectors.txt enthält genaue Bytes für einen Frame jedes Typs, für den Kopf der Rang-Datei, für eine COMMIT-Datei und die Negativfälle (zu große Länge, alte und künftige Inkarnation, unbekannter Typ, flags, service, membership epoch, ein reservierter Tag, ein Kollektiv-Frame mit Tag oder ohne Kopf, falsches SHA-256, falsches Magic). Einträge der Art reduce geben zwei Arrays, einen Elementtyp und eine Reduktion und das Ergebnis; sie decken Überlauf, Ordnung mit Vorzeichen, Ordnung der Fließkommazahlen, die logischen und bitweisen Operationen und die abgewiesenen Kombinationen ab. Signaturen lassen sich nicht festlegen (ML-DSA signiert mit Zufall); Vektoren mit Signatur tragen daher einen Platzhalter aus 0xaa-Bytes und nennen auch den signierten Teil. Die Datei wird von scripts/sperli-vectors.py geschrieben und geprüft. Ihr Format steht in ihrem Kopf.