SPERLI wire protocol, version 1

SPERLI (Simple Parallel Execution over Remote Links Interface) is the cluster layer of Hilde: a job of size ranks, each a process on a listed host, talking over TLS. It is not MPI-compatible. It takes only the semantics of MPI: rank, size, send and receive with a tag, barrier, broadcast, reduce, allreduce, gather and scatter. This document and the vectors in docs/sperli-vectors.txt are complete enough to write a second implementation, for example in pure C. German version: the Sperli protocol.

Scope and status

SPERLI is the own replacement of MPI for spreading computations, in pure C. It comes after the array type and the compute layer of Hilde, and nothing here is built ahead that depends on the array type.

All integers are little endian and unsigned unless said otherwise. A byte string is written [n] for n bytes. "Must" means the receiver refuses the frame, closes the connection and aborts the job, unless a rule says "drops".

Transport and security

Job description

The launcher tells each rank its job on the standard input of the process, as one line of lower-case hexadecimal digits and a line feed. The bytes behind the digits are:

FieldSizeMeaning
magic4SPJD
version21
reserved20
job id16the job id
size4the number of ranks, 1 to 4096
rank4the rank of this process, below size
incarnation41 or more
hostssize timesport (2 bytes, not 0), host length (2 bytes), host name
public key2 + 1344length and the CompositeSig public key of the job
token2 + tlength and the join token of this rank
CA4 + nlength and the PEM text of the job CA certificate
certificate4 + nlength and the PEM text of the certificate of this rank
key4 + nlength and the PEM text of the Ed25519 key of this rank

Numbers are little endian. A description with other bytes after the key, a cut field, a host that is refused, or a size, rank or incarnation out of range is refused. The key of the rank is a secret; the line is never written to a file by the launcher, and a rank writes the three PEM texts to files that only its user may read, in a directory of its own, for as long as it makes connections.

Connections

The rank with the lower number connects to the higher one. A rank listens at its port first, connects to every higher rank (trying again every 100 ms for 30 s, because the peer may not listen yet) and then accepts one connection from every lower rank. Both sides send their HELLO at once. A rank is silent for at most 5000 ms before it is taken as lost; a rank that waits for a frame sends nothing of its own in version 1, and a program that computes for longer than the limit raises it with the silence setting of the library.

Frame

Every message is a frame: a 52-byte header and length bytes of payload.

OffsetSizeFieldMeaning
04lengthbytes of payload after the header
41typeframe type, see below
51flags0 in version 1; others must be refused
62service0 = SPERLI; 1 = reserved for a shared Raft transport; others must be refused
816job id128-bit job id, the same for every frame of a job
244incarnation1 at launch, plus 1 at every restart
284membership epoch0 in version 1; reserved for dynamic membership; others must be refused
328epochcheckpoint epoch the frame belongs to; 0 for frames outside a checkpoint
404source rankrank of the sender
444target rankrank of the receiver
484taguser tag for DATA, 0 for all other types

Frame types

CodeNameDirectionPayload
1HELLOboth, first framecapabilities and join token
2DATAanymessage bytes
3COLLECTIVEanycollective header and data
4HEARTBEATbothsender counter
5ABORTanyreason code and text
6CKPT_BEGINrank 0 to allcheckpoint start
7CKPT_DATArank to buddy, or fetchchunk of a rank file
8CKPT_ACKrank to rank 0digest of the written file
9CKPT_COMMITrank 0 to allthe COMMIT file
10RESUMErank 0 to all, and answersresume epoch and status
11BYEbothempty

Code 0 and codes above 11 are invalid.

HELLO (1)

Sent by both sides straight after the TLS handshake; nothing else may come before it. A second HELLO is refused.

OffsetSizeField
02protocol version, 1
21language: 0 Hilde, 1 Go, 2 C
31reserved, 0
44services bit set: bit 0 point-to-point and collectives, bit 1 checkpoint, bit 2 reserved for Raft
84reserved, 0
124token length t
16tjoin token

The header of a HELLO carries the sender's rank and incarnation. The peer needs bit 0 in common; a protocol version other than 1 is refused.

Join token. The signed message is

OffsetSizeField
04magic SPJT
42token version, 1
62reserved, 0
816job id
244rank
284size
324incarnation
368expiry, seconds since 1970-01-01 UTC
442host length h, 1 to 255
46hhost name, ASCII

and the token is that message, then a 2-byte signature length (2484) and the signature. The receiver verifies the signature with the job public key and refuses the token if the signature fails, the job id is not its job, the rank is not the header rank and the rank of the certificate, size or incarnation differ from the job, the host differs from the host list entry of that rank, or the expiry is not later than the receiver's clock.

DATA (2)

The payload is the message. The tag is in the header. An empty message is valid.

COLLECTIVE (3)

Payload: a 16-byte collective header, then data. The tag of the frame header is 0, and a payload shorter than the collective header is refused with the frame header (before the payload is read). Data is at most 16777200 bytes, so that header and data fit the maximum payload.

OffsetSizeField
01operation: 1 barrier, 2 bcast, 3 reduce, 4 allreduce, 5 gather, 6 scatter
11element type: 0 bytes, 1 int32, 2 int64, 3 uint32, 4 uint64, 5 float32, 6 float64
21reduce operation: 0 none, 1 sum, 2 min, 3 max, 4 prod, 5 land, 6 lor, 7 lxor, 8 band, 9 bor, 10 bxor
31phase: 0 contribution towards the root, 1 result from the root
44root rank
88sequence number of the collective call, from 1; equal on all ranks for the same call
16ndata

Bytes, barrier, bcast, gather and scatter carry element type 0 and reduce operation 0. Reduce and allreduce carry the type and operation of the call. Every rank makes the same collective calls in the same order; the sequence number counts the calls of a rank from 1, and a call that fails still counts.

Shape. Version 1 uses a star, not a tree or a ring, so that the result does not depend on timing and every rank is already connected to every other. For allreduce the root is rank 0. Reduction runs over the contributions in ascending rank order, and the root's own value is the contribution of its rank. Rank r sends only to the root and receives only from the root, except the root, which talks to every rank.

OperationContributions (phase 0)Results (phase 1)
barrierempty, every rank to rank 0empty, rank 0 to every rank
bcastnonethe data of the root to every other rank
reducedata of every other rank to the rootnone; every rank but the root gets the empty string
allreducedata of every rank to rank 0the reduced data, rank 0 to every other rank
gatherthe block of every other rank to the rootnone; the root joins the blocks in rank order, the others get the empty string
scatternoneblock i of the root's data to rank i; the root keeps its own block

Reduce operations. The elements of one array are width bytes each (1, 4 or 8, by type), least significant byte first. Two arrays are reduced element by element, the first as the left operand.

OperationElement typesResult
sum, prodint32, int64, uint32, uint64the result modulo 2 to the power of the width in bits; nothing traps
sum, prodfloat32, float64the IEEE result in the width of the type
min, maxwhole numbersby the order of the type: signed for int32 and int64, unsigned for the others
min, maxfloatsmin takes the second operand only when it is smaller than the first, max only when it is larger
land, lor, lxorwhole numberseach element is true when it is not 0; the result is 0 or 1
band, bor, bxorwhole numbers and bytesthe bits

Not allowed, and refused by every rank before anything is sent (H1618): sum, min, max, prod and the logical operations on bytes; the logical and bitwise operations on floats; reduce operation 0; a type above 6; an operation above 10.

Argument checks. Every check that every rank can make alone is made before anything is sent, so that all ranks refuse the same call and the job goes on: a root of size or more, a type or operation that does not go together, and data that is not a whole number of elements (H1618). Checks that only one rank can make end the job:

Order with DATA. Collective frames and DATA frames of one connection arrive in the order sent. A rank that waits for a collective frame keeps the DATA frames before it for its own recv, and a rank that waits for DATA keeps the collective frames before it. The tags from 2147483648 are reserved for this: a DATA frame with such a tag is refused with the frame header (H1610), so a peer cannot pass a data frame off as a collective one.

HEARTBEAT (4)

Payload: 8 bytes, a counter of the sender that starts at 1 and grows by one per heartbeat. Each side sends one every 1000 ms on every connection when it has sent nothing else. A connection that has delivered no frame for 5000 ms is lost: the rank sends ABORT with reason 1 to all peers and stops.

ABORT (5)

OffsetSizeField
04reason: 1 heartbeat lost, 2 rank failed, 3 protocol error, 4 user abort, 5 checkpoint failed
4ntext, UTF-8, at most 1024 bytes

A rank that receives ABORT forwards nothing, closes all connections and ends the job step. With a checkpoint the launcher restarts the job from the last COMMIT (see Checkpoints).

BYE (11)

Empty payload. A rank sends BYE to every peer when it leaves normally and then closes the connection. A close without BYE is a lost peer.

CKPT_BEGIN (6)

Rank 0 to every rank, header epoch = the new epoch. A rank refuses a version other than 1 or a size other than its own.

OffsetSizeField
04checkpoint format version, 1
44size

CKPT_DATA (7)

OffsetSizeField
01kind: 0 buddy copy, 1 fetch request, 2 fetch reply, 3 fetch refused, 4 nothing to fetch
13reserved, 0
44owner rank, the rank whose file this is
88offset of the chunk in the file
168total length of the file
24nchunk

Kind 0: rank r sends its finished rank file in chunks in order to rank (r + 1) mod size, which stores it as rank-<r>.ckpt.buddy in its own epoch directory. Kind 1: a rank asks the buddy for a file it misses; offset and total are 0 and there is no chunk. Kind 2 answers with the chunks. Kind 3 means the buddy has no valid copy. Kind 4 says that a rank needs nothing, so that the buddy does not wait for a request. A chunk is at most 1 MiB; chunks come in order, the offset of each is the number of bytes received so far, and the total never changes.

CKPT_ACK (8)

Payload: 32 bytes, the SHA-256 of the whole rank file just renamed into place. Header epoch is the epoch.

CKPT_COMMIT (9)

Payload: the bytes of the COMMIT file of the epoch. Receivers verify the signature and store a copy.

RESUME (10)

OffsetSizeField
04size
44status: 0 request or ready, 1 file fetched from the buddy, 2 failed
832SHA-256 of the COMMIT file, 32 zero bytes for none
40nthe COMMIT file, for status 0 from rank 0 and for status 3

Status 3 is an offer: every rank other than 0 sends rank 0 the best COMMIT file it holds (header epoch = its epoch, empty when it holds none). Rank 0 takes the highest epoch among the offers and its own files whose signature verifies and sends status 0 with that epoch (header epoch) and the COMMIT file. Every rank answers to rank 0 with 0 or 1 once its state is loaded, or 2 if it cannot load it, and rank 0 then sends status 0 to all to go on; status 2 aborts the job.

Checkpoints

Layout on disk: <dir>/<job>/<epoch>/rank-<n>.ckpt, and <dir>/<job>/<epoch>/COMMIT. <job> is the job id as 32 lower-case hex digits; <epoch> is a decimal number, starting at 1.

Rank file. A header of 80 bytes, then the state.

OffsetSizeField
04magic SPCK
42format version, 1
62reserved, 0
816job id
248epoch
324rank
364size
408payload length
4832SHA-256 of the payload
80npayload (the serialized state)

A reader refuses a file with another magic or version, with a job id, epoch, rank or size that does not match what it expects, with a file length other than 80 plus the payload length, or whose payload does not hash to the stored SHA-256.

COMMIT file.

OffsetSizeField
04magic SPCM
42format version, 1
62reserved, 0
816job id
248epoch
324size
3632 * sizeSHA-256 of the whole rank file of rank 0, 1, ...
36 + 32 * size2signature length (2484)
38 + 32 * size2484CompositeSig signature over bytes 0 up to 36 + 32 * size

An epoch without a COMMIT whose signature verifies against the job public key, whose job id, epoch and size match, and whose hashes match the rank files does not count.

Procedure. For the collective checkpoint(state), with no open send or receive handle on any rank:

  1. Rank 0 sends CKPT_BEGIN with the new epoch e to all ranks.
  2. Each rank serializes its state and writes the rank file to a temporary name in the epoch directory, calls fsync, renames it to rank-<n>.ckpt, and sends CKPT_ACK with its SHA-256 to rank 0.
  3. Each rank sends the file to its buddy (n + 1) mod size as CKPT_DATA kind 0.
  4. When rank 0 has all ACKs it writes COMMIT the same way (temporary name, fsync, rename), signed with the job key, and sends CKPT_COMMIT to all ranks. Each rank verifies the signature, checks that the COMMIT lists the SHA-256 of its own file and of the buddy copy it holds, and keeps a copy. A rank leaves checkpoint when it has the COMMIT. Rank 0 removes an old COMMIT of the same epoch before it sends CKPT_BEGIN, so a half-made epoch never shows a COMMIT from an earlier try.
  5. Only epochs e and e - 1 are kept. After the COMMIT of e, every older directory, committed or not, is deleted. Directories of a later epoch are left alone, because a rank that is further on may be writing them.

A checkpoint with a send or receive still open fails with H1624 on every rank, which is checked at run time; the state goes through the type class Checkpointable, and a type that holds a handle, a connection or a file has no instance, so the compiler refuses the call. A failed checkpoint sends ABORT with reason 5 to the others.

Failure and resume. On heartbeat loss all ranks abort. The launcher restarts the job with the incarnation plus 1, at most --restart-max times; a lost rank may go to another host of the list, never to a refused host (or a host of the deny file). The resume epoch is the highest epoch with a valid COMMIT. On resume the signature of the COMMIT file is verified against the job public key, and the SHA-256 of every rank file is checked twice: against the header of the file and against the entry of the COMMIT file. A rank whose file is missing or invalid fetches it from its buddy with CKPT_DATA kind 1; the fetched file is checked the same way. A failure is refused with a code: H1619 for a file that is not the one expected, H1620 for a SHA-256 that does not match, H1621 for a COMMIT that is malformed or whose signature does not hold, H1622 when no epoch can be resumed. Version 1 requires the same size at resume; a signed COMMIT of another size is refused with H1623. When the newest epoch has no valid COMMIT but an older one has, the older one is resumed; when a COMMIT fails and no other holds, the code of that failure is given.

Version 1 limits and reserved fields

Fixed: membership of size ranks; service 0; membership epoch 0; flags 0. These are reserved for a shared Raft and SPERLI transport and for dynamic membership and are not interpreted yet. A receiver of version 1 refuses nonzero values, so a later version must announce itself in HELLO first.

Test vectors

docs/sperli-vectors.txt holds exact bytes for a frame of every type, for the rank file header, for a COMMIT file, and the negative cases (oversized length, old and future incarnation, unknown type, flags, service, membership epoch, a reserved tag, a collective frame with a tag or without its header, bad SHA-256, bad magic). Records of kind reduce give two arrays, an element type and a reduce operation, and the result; they cover wrapping, signed order, float order, the logical and bitwise operations and the refused combinations. Signatures cannot be fixed (ML-DSA signs with randomness), so vectors with a signature carry a placeholder of 0xaa bytes and also give the signed part. The file is written and checked by scripts/sperli-vectors.py. The format of the file is explained at its head.