6.5.4. Group Operations: Theory and Implementation
Note
This is a developer-level specification. It describes how the
PMIx_Group_* family is realized inside libpmix — the client-side
and server-side machinery, the two-phase collective model, and the
fault-tolerance accounting that keeps a group operation from hanging when
a participant is lost. The user-facing semantics are documented in
Group Construction, Destruction, and Fault Tolerance; the general server-side collective-tracking
model that the group family reuses is specified in
Specification: Tracking Local Collective Participation.
Audience: contributors modifying the group code, the collective-tracking
service, or a host environment’s pmix_server_module_t.group callback.
6.5.4.1. Two realizations of “group construction”
The group family is implemented as two distinct mechanisms, chosen per API rather than per group:
Round-trip collectives.
PMIx_Group_constructandPMIx_Group_destructare true collectives. Each participant’s client library packs a command and sends it to its local server withPMIX_PTL_SEND_RECV; the server assembles the local participants, forwards the aggregated operation to the host for global completion, and replies to each local participant. Commands:PMIX_GROUP_CONSTRUCT_CMDandPMIX_GROUP_DESTRUCT_CMD.Event-driven handshakes.
PMIx_Group_invite,PMIx_Group_join, andPMIx_Group_leavesend no server command. They are realized entirely through the event-notification subsystem (PMIx_Notify_event), with the server’s only role being to relay events between its clients and the host. This is why the invite method works without any host collective support, and whyPMIx_Group_leavewas re-implemented as an event (its formerPMIX_GROUP_LEAVE_CMDround-trip was never handled by any server switchyard and has been removed).
The code lives in two files:
src/client/pmix_client_group.c— all six client API pairs, the client group tracker, and the client-side fault handling (invite timeout/decline, construct abort, leader-failure watch).src/server/pmix_server_group.c— the server block/tracker structures, the local-phase assembly and completion predicate, host handoff, and the server-side fault accounting.
Shared membership helpers used by the collectives live in
src/client/pmix_client_convert.c.
6.5.4.2. Client side
6.5.4.2.1. The group tracker
A single caddy type, pmix_group_tracker_t (in
pmix_client_group.c), carries state across the progress-thread boundary
for every API in the family. It is deliberately overloaded — its meaning
shifts with the operation — but always carries the standard caddy machinery
(ev, lock, status, cbdata, cached cbfunc/opcbfunc)
plus the group identity (grpid, members/nmembers). For the
event-driven paths it additionally holds the per-member bookkeeping used by
the invite/join handshake (responded[], answered[], nanswered,
optional), the timeout state (timer_active), a one-shot resolution
guard (completed), and the event-handler registration id (ref, seeded
to SIZE_MAX so teardown can tell whether a handler was ever registered).
6.5.4.2.2. Construct and destruct (round-trip)
PMIx_Group_construct_nb does the real work; the blocking form is a thin
wrapper that supplies an internal callback and waits on the caddy lock. The
non-blocking form:
Expands and validates membership. When the participants are named explicitly (not the add-members or bootstrap methods), the
procsarray is run throughpmix_client_convert_group_procs()so any embedded PMIx group identifier is replaced by its member processes, and the caller is checked for inclusion viapmix_client_proc_is_included(). A caller that is not among the expanded participants getsPMIX_ERR_NOT_A_MEMBERimmediately. Both steps are skipped whenPMIX_GROUP_ADD_MEMBERSorPMIX_GROUP_BOOTSTRAPis present, since those intentionally omit participants.Packs the message (
construct_msg()): the command, group ID, participant array, and info array, folding in the caller’s own endpoint data (fetched atPMIX_REMOTEscope and packed asPMIX_PROC_DATA) and prepending the caller’s proc ID to anyPMIX_GROUP_INFOarray.Dispatches with
PMIX_PTL_SEND_RECV(..., construct_cbfunc, cb)and returns.
construct_cbfunc unpacks the returned status, final membership, optional
context ID, and group info, registers the completed group in
pmix_client_globals.groups (sorted for a consistent cross-participant
view), and delivers the results. PMIx_Group_destruct_nb mirrors this with
PMIX_GROUP_DESTRUCT_CMD, sending the membership it holds locally (the
server does not store it) and removing the group from the local list on
completion.
The persistent pmix_group_t also carries the group’s failure policy across
the two collectives. PMIX_GROUP_NOTIFY_TERMINATION is specified at construct
time but governs the eventual destruct, and the server keeps no group state
between operations — so the blocking construct wrapper captures the flag into
the tracker, add_group() records it in the pmix_group_t (notterm),
and PMIx_Group_destruct_nb re-attaches PMIX_GROUP_NOTIFY_TERMINATION to
the destruct request (unless the caller passed it explicitly, which overrides).
The server then reads it off the aggregated destruct block via
grp_notify_termination().
pmix_client_proc_is_included() (in pmix_client_convert.c) is the
shared “is the caller covered by this participant array?” predicate — it
matches the caller’s namespace and accepts PMIX_RANK_WILDCARD,
PMIX_RANK_LOCAL_NODE, PMIX_RANK_LOCAL_PEERS, or an exact rank. It was
hoisted out of duplicate copies in the fence and connect/disconnect paths so
all three collectives share one membership check.
6.5.4.2.3. Invite / join / leave (event-driven)
The invite method tracks invitees by identity. invite_setup()
expands wildcard ranks into concrete members, builds parallel
responded[] (accepters) and answered[] (accepters and decliners
and terminated) flag arrays, seeds the leader’s own slot, and registers a
prepended handler for PMIX_GROUP_INVITE_ACCEPTED,
PMIX_GROUP_INVITE_DECLINED, and PMIX_PROC_TERMINATED. It then notifies
the invitees with PMIX_GROUP_INVITED over a custom range, and, if a
PMIX_TIMEOUT was supplied, arms a libevent timer.
The critical thread-safety rule: the response handler (invite_handler)
and the timeout handler run on the progress thread and must never raise
an event themselves (that would deadlock). They only wake the operation
(invite_wake, guarded by the completed one-shot). All event
generation — PMIX_GROUP_INVITE_FAILED for non-accepters, the
PMIX_GROUP_CONSTRUCT_COMPLETE broadcast, or a PMIX_GROUP_CONSTRUCT_ABORT
— happens afterward on the caller’s thread in invite_announce(). A
termination event names the departed proc in PMIX_EVENT_AFFECTED_PROC
(not the event source), so invite_handler attributes it to the right
member via that key.
PMIx_Group_join_nb maps the pmix_group_opt_t to an event code
(PMIX_GROUP_INVITE_ACCEPTED / PMIX_GROUP_INVITE_DECLINED) and notifies
the leader. On an accept with a known leader it also arms the leader-failure
watch (below).
PMIx_Group_leave_nb finds the group locally, drops it from the local list
immediately, and generates a PMIX_GROUP_LEFT event ranged to the
membership excluding self (naming self in PMIX_EVENT_AFFECTED_PROC). Per
its contract it returns once the event is locally generated. Remaining
members update their local membership when they receive PMIX_GROUP_LEFT,
handled in pmix_invoke_local_event_hdlr (src/event/pmix_event_notification.c)
alongside the existing group-construct-complete membership handling.
6.5.4.2.4. The leader-failure watch
When a process accepts an invitation, setup_leader_watch() registers a
private, non-blocking, prepended handler keyed on leader termination
(PMIX_PROC_TERMINATED, PMIX_ERR_PROC_ABORTED,
PMIX_ERR_PROC_TERM_WO_SYNC) and on construct resolution
(PMIX_GROUP_CONSTRUCT_COMPLETE / PMIX_GROUP_CONSTRUCT_ABORT). If the
watched leader (held in members[0]) is lost before the construct
resolves, the handler emits PMIX_GROUP_LEADER_FAILED to this process’s own
handlers via non-blocking PMIx_Notify_event (the blocking form would
deadlock on the progress thread) and tears itself down. It always returns
PMIX_EVENT_NO_ACTION_TAKEN so it never swallows the events the application
registered to see. Reselection is entirely application-driven: a process
nominates itself by returning PMIX_GROUP_LEADER in its handler’s results,
and the outcome is announced with PMIX_GROUP_LEADER_SELECTED.
6.5.4.3. Server side
6.5.4.3.1. Block and tracker structures
Group collectives use a two-level tracker (unlike the flat
pmix_server_trkr_t shared by fence/connect/disconnect), because a single
group construct can be assembled from several distinct call signatures
(leader, followers, bootstrap) and carries group-only state:
grp_block_t— one per group-ID operation, on the server-global listpmix_server_globals.grp_collectives(declared insrc/server/pmix_server_ops.h). It owns: the operation kindgrpop(apmix_group_operation_t, initialized toPMIX_GROUP_NONE); the aggregated membership (pcs/npcs) and directives (info/ninfo) built byaggregate_info; the listmbrsofgrp_trk_t, one per participant call (its size is the number of local contributions); the listdepartedof processes lost before contributing;nlocal(the expected local count) anddef_complete(all participating namespaces registered, sonlocalis final);host_called(the local phase is forwarded and frozen);need_cxtid; and the local-phase timeout state (ev,event_active).grp_trk_t— one per participant call, hung off a block’smbrs. It holds the membership and directives as passed by that participant, a back-pointer to its block, andlocal_cbs— the list ofpmix_server_caddy_tthat both identifies who actually contributed (by peer name) and holds the reply destination for each local participant.grp_shifter_t— the thread-shift caddy carrying the host’s completion result back onto the progress thread.
6.5.4.3.2. Local-phase assembly and completion
pmix_server_group() receives a construct/destruct command on the progress
thread. It requires a host group callback (else PMIX_ERR_NOT_SUPPORTED),
unpacks the group ID / procs / directives, seeds a default
PMIX_LOCAL_COLLECTIVE_STATUS = PMIX_SUCCESS slot as the last element of
the local info array (the slot the fault machinery later overwrites), and
locates or creates the block via get_tracker(). A call with nprocs ==
0 is a follower; a PMIX_GROUP_BOOTSTRAP call is a bootstrap leader —
both keep independent trackers (the leader is unknown) and are forwarded to
the host immediately, one call per participant, without local aggregation.
For the collective (fully-specified) method:
check_definition_complete()walks all trackers’ member procs, verifies every participating namespace is known and fully registered, and — once so — setsdef_completeand the finalnlocal(wildcards contribute the namespace’s local process count). Until every namespace is registered the block stays pending, so a client that calls in before its localregister_clientevent cannot complete the collective early.grp_blk_locally_complete()is the single completion predicate:def_complete AND (size(mbrs) + size(departed)) >= nlocal
This is the group analog of
pmix_server_trk_complete()used by the fence family. The>=(rather than==) tolerates fork/exec clones that share a rank and over-count. The local phase is complete once every expected local participant has either contributed (a tracker onmbrs) or departed.When the predicate holds and the block has not already been forwarded,
aggregate_info()merges every tracker’s procs/info into the block (deduplicating and special-casingPMIX_GROUP_ADD_MEMBERS,PMIX_GROUP_BOOTSTRAP,PMIX_PROC_DATA, andPMIX_GROUP_INFO), the fault decision below is applied, the local-phase timer is deleted,host_calledis set, and the operation is forwarded topmix_host_server.group().
The host’s completion callback grpcbfunc thread-shifts into
_grpcbfunc, which extracts the context ID, final membership, and group
info from the host result, stores group info via
pmix_server_process_grpinfo, and — for every block matching the ID
(bootstrap can create several) — packs each tracker’s local_cbs reply and
queues it, then removes and releases the block.
6.5.4.4. Fault tolerance: identity-based accounting
The group family reuses the identity-based collective-tracking model
specified in Specification: Tracking Local Collective Participation. The essential idea is that
local participation must be tracked by participant identity, never by a
bare counter, so the lost-connection handler can answer the one question a
counter cannot: “has this specific participant already contributed?” The
completion predicate counts contributed ∪ departed against expected;
the two lost-participant rules are:
Case A — the participant had already contributed. Ignore the loss for this collective. The contribution stands, its data stays in the assembled result, and
nlocalis not reduced. The dead peer’s queued reply is harmless (its socket is closed). This is the rule that fixes the historical early-completion / data-loss bug.Case B — the participant had not yet contributed. It never will, so record it on
departed(once, deduplicated). Completion is then re-evaluated; if the block is now locally complete it is forwarded exactly as on a normal final contribution.
Two entry points reach the same per-block routine account_departed():
pmix_server_grp_peer_lost(peer)— called fromlost_connection()insrc/mca/ptl/base/ptl_base_sendrecv.cwhen a socket drops. A lost connection is not scoped to one group, so it appliesaccount_departedto every block ongrp_collectives. (Before this work the lost-connection handler walked only the fence-familycollectiveslist, so a lost group member hung the construct forever.)pmix_server_grp_member_left(grpid, proc)— the deliberate cousin. APMIx_Group_leavegenerates aPMIX_GROUP_LEFTevent that the originating server intercepts insrc/server/pmix_server_ops.c(before loop-detection, so only the originating server acts). It appliesaccount_departedto the blocks for the named group only.
account_departed() skips bootstrap/follower blocks (nlocal == 0) and
already-forwarded blocks (host_called); determines membership by matching
proc against each tracker’s pcs with PMIX_CHECK_NAMES and prior
contribution by matching against each tracker’s local_cbs peer names
(Case A vs. Case B); and, if the loss completes the local phase, aggregates,
applies the fault decision, and forwards. Callers iterate with a
FOREACH_SAFE macro because the block may be released on error.
6.5.4.4.1. The abort-vs-survive gate
At each point where a loss (or leave) completes a block’s local phase, the server decides between aborting and surviving:
if (0 < pmix_list_get_size(&blk->departed)) {
if (PMIX_GROUP_CONSTRUCT == op && !grp_ft_collective(blk)) {
abort_construct(blk, PMIX_GROUP_CONSTRUCT_ABORT);
return;
}
if (!(PMIX_GROUP_DESTRUCT == op && grp_notify_termination(blk))) {
pmix_server_set_collective_status(blk->info, blk->ninfo,
PMIX_ERR_LOST_CONNECTION);
}
}
grp_ft_collective(blk)scans the aggregated directives forPMIX_GROUP_FT_COLLECTIVE("pmix.grp.ftcoll", bool, default false);grp_notify_termination(blk)scans them forPMIX_GROUP_NOTIFY_TERMINATION("pmix.grp.notterm", bool, default false).A construct without the FT flag aborts. Every other case survives on the survivors. A destruct with termination notification completes cleanly: the
PMIX_LOCAL_COLLECTIVE_STATUSslot is left atPMIX_SUCCESSand the survivors are told which member was lost via the synthesizedPMIX_GROUP_MEMBER_FAILEDevent (the event standing in place of an error). Every other survive path — a fault-tolerant construct, or a destruct without notification — recordsPMIX_ERR_LOST_CONNECTIONinto that slot so the host is told the local phase was degraded (clients still receive their own status throughgrpcbfunc).
abort_construct(blk, status) deletes any armed timer; if a host is
present it issues pmix_host_server.group(PMIX_GROUP_CANCEL, blk->id, ...)
with cancel_cbfunc; sets host_called; and completes the local
participants directly by driving grpcbfunc with the abort status. The
PMIX_GROUP_CANCEL call is essential in the cross-server case: other
servers that already forwarded their local phase are waiting on the host
collective for a contribution this server will never send, so the host must
be told to unstick that collective. Because the block is removed locally, the
host’s resulting abort release is a no-op back here — no double completion.
abort_construct is only ever called pre-forward, so no host completion can
race it.
6.5.4.4.2. Synthesizing PMIX_GROUP_MEMBER_FAILED
On the survive path, notify_local_members_of_loss() emits, for each
departed process, a PMIX_GROUP_MEMBER_FAILED event carrying
PMIX_EVENT_AFFECTED_PROC (the lost member) and PMIX_GROUP_ID. It is
delivered through the internal, progress-thread-safe
pmix_server_notify_client_of_event — never the public PMIx_Notify_event.
Because each server does this accounting for its own local survivors, a member
lost on another node is reported by that node’s server performing the identical
logic; the feature therefore needs no cooperation from the host environment.
_grpcbfunc calls it when the operation completed successfully (status is
PMIX_SUCCESS), has a non-empty departed list, and is either a construct
(which reaches _grpcbfunc with success only on the fault-tolerant survive
path — an aborted construct arrives with PMIX_GROUP_CONSTRUCT_ABORT) or a
destruct for which grp_notify_termination() reports that
PMIX_GROUP_NOTIFY_TERMINATION was requested. For the destruct survive path
the two host-forward points (pmix_server_group and account_departed)
deliberately leave the local-collective status at PMIX_SUCCESS when
notification was requested (so the operation returns success, the event standing
in place of an error) and record PMIX_ERR_LOST_CONNECTION otherwise.
6.5.4.4.3. The local-phase timeout
A PMIX_TIMEOUT directive arms a libevent timer over the local phase via
PMIX_THREADSHIFT_DELAY(blk, group_timeout, tmo), setting blk->ev and
event_active. group_timeout() fires abort_construct(blk,
PMIX_ERR_TIMEOUT). The timer is deleted before every host-forward (and in
the block destructor gbdes as an error-path safety net), so it can never
fire on an already-forwarded — and possibly freed — block. As with the fence
family, this timer covers only the local phase; the host owns the
cross-server timeout.
6.5.4.4.4. The collective-status slot
pmix_server_set_collective_status() (in src/server/pmix_server_ops.c)
locates the PMIX_LOCAL_COLLECTIVE_STATUS entry by key (not by
position) and writes the status in place; an absent slot is a no-op. Finding
it by key matters for connect, which appends per-participant and job-level
info after the seeded slot — a positional write would corrupt that appended
data and leave the real status stale. The same helper is used by the
fence-family lost_connection path, both group host-forward points, and
the white-box unit test test/unit/collective_status.c.
6.5.4.5. The host callback boundary
Group construct/destruct/cancel all cross the PMIx-to-host boundary through
one callback slot, pmix_server_module_t.group, whose operation is a
pmix_group_operation_t:
typedef enum {
PMIX_GROUP_CONSTRUCT, /* 0 */
PMIX_GROUP_DESTRUCT, /* 1 */
PMIX_GROUP_NONE, /* 2 */
PMIX_GROUP_CANCEL /* 3 */
} pmix_group_operation_t;
Because the enum’s numeric values cross that boundary, new values must be
appended, never inserted — PMIX_GROUP_CANCEL was added at the end.
PMIx_Group_operation_string() (in src/common/pmix_strings.c)
stringifies each value. Two footguns to respect when editing this enum:
Keep the body free of comment lines: the Python bindings generator
bindings/python/construct.pytreats every line inside atypedef enumas an enumerator, so an interleaved comment shifts every subsequent value. The explanatory comment therefore lives in a block above thetypedef.PMIX_GROUP_CANCELis not carried on the client-to-server wire (it is purely a PMIx-to-host callback op), so no newbfropssupport is needed.
6.5.4.5.1. Capability detection
The whole feature set above is advertised by the PMIX_CAP_GROUP_FT
capability flag in include/pmix_version.h.in. Its presence in the
installed pmix_version.h is the signal a consumer (e.g. PRRTE, via
PRTE_CHECK_PMIX_CAP) tests for; absence means the running PMIx predates
group fault tolerance. Edit the .in template, never the generated header.
6.5.4.6. Event and attribute reference
Events (defined in include/pmix_common.h.in):
Event |
Value |
Role |
|---|---|---|
|
-159 |
Leader → invitees; carries |
|
-160 |
Departing proc → members; server intercepts to account the leave |
|
-161 |
Invitee → leader ( |
|
-162 |
Invitee → leader ( |
|
-163 |
Leader-local report of a non-accepter |
|
-164 |
Membership change notification |
|
-165 |
Construct aborted; all participants notified |
|
-166 |
Group formed; carries final |
|
-167 |
App-driven reselection result |
|
-168 |
Leader lost; surfaced to a joining member’s handlers |
|
-169 |
Context ID assigned |
|
-170 |
Server-synthesized on a lost member (survive path) |
Key attributes / status codes:
PMIX_GROUP_FT_COLLECTIVE("pmix.grp.ftcoll", bool) — the survive-vs-abort gate for a lost member during construct.PMIX_GROUP_OPTIONAL(bool) — all-or-nothing (default) vs. reduced group for the invite method.PMIX_GROUP_NOTIFY_TERMINATION(bool) — report vs. error a member lost mid-destruct.PMIX_GROUP_LEADER(bool) — declares the construct leader; also returned from a leader-failed handler to nominate a replacement.PMIX_LOCAL_COLLECTIVE_STATUS(pmix_status_t) — the per-tracker health slot the fault machinery updates by key.PMIX_ERR_NOT_A_MEMBER— returned when the caller is not among the expanded construct participants.PMIX_ERR_TIMEOUT— local-phase timeout expiry.PMIX_ERR_LOST_CONNECTION/PMIX_ERR_PARTIAL_SUCCESS— degraded local-collective status recorded for the host.
6.5.4.7. Testing
The behaviors above are exercised at several levels:
test/unit/collective_status.c— white-box coverage of the by-key status helper (connect/fence layouts, absent slot, degenerate inputs), wired intomake check.test/simpledrivers —simpgrpdie(construct survives loss +PMIX_GROUP_MEMBER_FAILED, withPMIX_GROUP_FT_COLLECTIVE),simpgrpddie(destruct survives loss +PMIX_GROUP_MEMBER_FAILEDand returns success, withPMIX_GROUP_NOTIFY_TERMINATION),simpgrpcabort(default abort withPMIX_GROUP_CONSTRUCT_ABORT), andsimpgrpctimeout(PMIX_ERR_TIMEOUTon a hung live member).examples/exercisers driven by the dockerswarm harness (contrib/dockerswarm/run-group-events.sh) across a multi-node swarm, so the cross-server notification and cancel paths actually run:group_invite(happy path),group_invite_timeout/group_invite_decline/group_invite_abort(invite fault paths),group_destruct_dieandgroup_die(loss mid-destruct/construct),group_construct_abort(lost member forcesPMIX_GROUP_CANCELto unstick remote servers),group_daemon_fail(whole-daemon loss), andgroup_leave.