Skip to content

grpcomm: a survivable job's fences stop waiting for departed processes - #2881

Merged
rhc54 merged 5 commits into
openpmix:masterfrom
rhc54:topic/fence-dead-daemon-procs
Nov 3, 2026
Merged

rhc54 merged 5 commits into
openpmix:masterfrom
rhc54:topic/fence-dead-daemon-procs

Conversation

@rhc54

@rhc54 rhc54 commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

The DVM half of making a survivable job's fences stop waiting for processes that have gone - a ULFM job that lost a process hung in MPI_Finalize. The PMIx half is openpmix/openpmix#4391.

A daemon whose processes have all departed

A whole-namespace fence counts every daemon hosting one of the namespace's processes, and a daemon contributes once its PMIx server has heard from every local participant. The PMIx server takes a dead client out of that count, so a daemon that loses some of its processes still contributes. One that loses all of them has nobody left to call the fence and never contributes, and its parent's rollup waited for it forever.

Each daemon now reports the termination of a process it hosts up the routing tree (PRTE_RML_TAG_FENCE_DEPARTED), and a rollup short of its count asks who it is still waiting for: a participant is awaited unless it has departed, its daemon has failed, or its subtree has reported. Up the tree rather than by broadcast, because a process that called the fence and then died has its daemon's contribution on the same link, ahead of the news of its death; a tracker records which subtrees reported, so nothing is counted twice. Only PRTE_JOB_RECOVERABLE / PRTE_JOB_CONTINUOUS jobs, nothing is spent when nothing has departed, and the fence's status is left alone - failing it would make every survivor's PMIx server discard the data the survivors contributed.

Why it went unseen: the HNP already drops an emptied node from its own copy of the job map, so in any DVM where it is every daemon's parent - up to rml_base_radix + 1 daemons, 65 by default - the hang never showed.

A daemon whose own share of the job has ended

job_teardown gives up a job's map when the daemon's processes of the job are done, while the job runs on elsewhere - and create_dmns() refused any fence over a job with no map on a daemon other than the controller, dropping the contributions of every daemon below it. A wildcard is now resolved through the job's processes, as a named rank always was. This one is not specific to failures.

Retiring and restarting clients

  • A daemon now deregisters each terminated client with its PMIx server, as the HNP always has - otherwise a rank that finalized and exited stays expected until its whole job is deregistered. Only against a PMIx with PMIX_CAP_DEREG_DEPARTS (new configure check); against an older one, deregistering before the server reads the closed socket causes the very hang this fixes.
  • A process restarted under its old rank is a participant again. restart_proc now reports the return up the tree (prte_grpcomm_proc_restarted()) and registers the rank with PMIx again, before the new process exists - termination deregisters it, on the HNP today, and the server refuses a rank it does not have. A recovery restart rebuilds the departed set from each daemon's own processes rather than topping it up, so a return lost with a failed daemon cannot leave a stale departure; notices carry the recovery epoch. The termination hooks sit outside the RECORDED guard, so a restarted process's next death is reported whether or not the restart resets it - that, and num_terminated, are left for whoever rewires the restart path (noted in src/mca/odls/AGENTS.md).

Testing

  • test/unit/grpcomm: test_fence_departed (completion on departure, a restart, the rebuild, the epoch) and test_fence_tracker_unmapped_daemon; with each fix disabled its cases fail.
  • make check: 29 pass, 0 fail.
  • dockerswarm, four nodes one rank each, --prtemca rml_base_radix 2 --rtos recoverable, one rank SIGKILLed after a fence, the survivors then fencing over the whole job: on master the survivors time out with nothing completed; with this, they complete with PMIX_SUCCESS whichever rank is killed. No-death and the default-radix three-node case complete; a job that is not recoverable still aborts at its first loss. The harness's core and server suites pass.

Not covered: the group collective over a whole namespace has the same shape, but whether it may complete without a member is the caller's (PMIX_GROUP_FT_COLLECTIVE), so it needs its own policy. And a restart is unit-tested only, since nothing invokes restart_proc yet.

rhc54 added 5 commits November 2, 2026 17:42
A fence over a whole namespace - PMIx_Fence(NULL, 0), and so
MPI_Finalize - counts every daemon hosting one of its processes, and a
daemon contributes once its PMIx server has heard from every local
participant. The PMIx server takes a terminated client out of that
count, so a daemon that loses some of its processes still contributes.
One that loses all of them has nobody left to call the fence and never
contributes, so the rollup waited for it forever. A job that ends at its
first loss never noticed; in a survivable one - a ULFM application -
every survivor hung in MPI_Finalize.

Each daemon now reports the termination of a process it hosts up the
routing tree (PRTE_RML_TAG_FENCE_DEPARTED), and passes up what its
children report. A rollup short of its count then asks who it is still
waiting for: a participant is awaited unless it has departed, its daemon
has failed, or its subtree has reported. Up the tree rather than by
broadcast because a process that called the fence and then died has its
daemon's contribution on the same link, ahead of the news of its death;
and a tracker records which subtrees reported, so a subtree that
reported and then lost its processes is never counted twice. The HNP
records only the processes it hosts, for the same ordering reason.

Only for PRTE_JOB_RECOVERABLE or PRTE_JOB_CONTINUOUS jobs, and the
fence's status is left alone: a departed process is no longer a
participant, as the PMIx server treats a whole-job fence started after a
client died, and failing the fence would make every survivor discard
the data the survivors contributed. Nothing is spent when nothing has
departed. A recovery restart resends the set to the new parent.

test_fence_departed in test/unit/grpcomm covers it over a hand-built
routing tree; with the new rule disabled seven of its checks fail.

Signed-off-by: Ralph Castain <rhc@pmix.org>
A daemon told PMIx nothing when one of its clients terminated, and left
it to the socket close - which the server reads as a loss only when the
client had not finalized. A rank that finalized and then exited
therefore stayed an expected participant in its job's collectives until
the whole job was deregistered, and a whole-job fence its peers made
meanwhile waited for it. The HNP has always deregistered the clients it
hosts; the daemons now do the same, without blocking, from the
termination accounting.

Only against a PMIx that advertises PMIX_CAP_DEREG_DEPARTS. Against one
that does not, deregistering a client before the server has read its
closed socket marked the rank finalized without taking it out of any
collective - the very hang this avoids - so there the daemon leaves it
to the socket close, as before.

Signed-off-by: Ralph Castain <rhc@pmix.org>
state/prted's job_teardown gives up a job's map when this daemon's own
processes of the job have all terminated, and keeps the job object -
the job goes on elsewhere. But the daemon still relays the job's
collectives for its subtree, and create_dmns() refused any fence over
a job with no map on a daemon other than the controller. So the
contribution of every daemon below one whose processes had finished was
dropped with "Not found", and a fence over the job hung for good.

Only a wildcard needs the map: a named rank is already resolved through
the job's processes. Resolve a wildcard the same way when the map is
gone - every process stays placed on its node, and so on its daemon,
until the DVM says the job is over. A daemon that has not been told of
the job's processes yet has nothing to walk and is refused as before.

Seen on a four-daemon DVM with rml_base_radix 2 and a survivable job
whose rank on daemon 1 - the parent of daemon 3 - was killed: the
survivors' next whole-job fence never completed.
test_fence_tracker_unmapped_daemon fails without this.

Signed-off-by: Ralph Castain <rhc@pmix.org>
The HNP drops an emptied node from a job's map itself, so a DVM in
which it is every daemon's parent - anything up to rml_base_radix + 1
daemons, 65 by default - never showed the hang the departed set fixes.
Record that, the relay-map fix beside it, and how to reproduce both on
four nodes.

Signed-off-by: Ralph Castain <rhc@pmix.org>
A process the runtime restarts under its old rank is a participant
again, but the departed set still holds its departure - and that is the
dangerous error: a survivable job's fence would stop waiting for the
live process and complete without it.

prte_grpcomm_proc_restarted() clears the departure and sends a return
up the routing tree, on the same links the departure took, so it
reaches every daemon that heard of it and reaches each ahead of
anything the new incarnation contributes. restart_proc calls it before
the new process exists, and also registers the rank with the PMIx
server again: termination deregisters the client - on the HNP as on a
daemon - and the server refuses a rank it does not have, so without
this the restarted process could not initialize; with a PMIx that has
PMIX_CAP_DEREG_DEPARTS it is also what counts the rank back into the
server's collectives.

A return can be lost with a failed daemon, leaving a stale departure on
an ancestor that a repaired tree may make our new parent. So a recovery
now rebuilds the set rather than topping it up: every daemon drops what
others reported, keeps its own processes' departures and reports those
again. Notices carry the recovery epoch, so one from before the
recovery is dropped and one from after it that overtakes our own
notice of the recovery is applied after our rebuild, not wiped by it.

The termination hooks move outside the RECORDED guard on a daemon, and
the grpcomm one on the HNP: each is a no-op the second time for one
incarnation, and a restarted process's next termination has to be
reported whether or not the restart reset RECORDED, which is left to
whoever rewires restart_proc.

test_fence_departed covers the return, the rebuild and the epoch; with
either half disabled, its checks fail.

Signed-off-by: Ralph Castain <rhc@pmix.org>
@rhc54
rhc54 merged commit c18bda6 into openpmix:master Nov 3, 2026
18 checks passed
@rhc54
rhc54 deleted the topic/fence-dead-daemon-procs branch November 3, 2026 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant