6.9. Output Forwarding for Spawned Jobs
This page describes how the output of a dynamically spawned job is routed, what the current implementation does and does not do, and what is left. It is a working design document rather than a description of finished behavior. The PMIx side is implemented; the items still marked Planned - PRRTE are not, and the “What is missing” section below is kept as the description of the defect each fix closed rather than as a statement of current behavior.
6.9.1. The behavior we want
A job spawned through PMIx_Spawn should have its output forwarded according to the following precedence, highest first:
The output attributes the spawn request carried. If the request named any of
PMIX_FWD_STDOUT,PMIX_FWD_STDERRorPMIX_FWD_STDDIAG, that request is honored exactly as written - including a channel explicitly set tofalse, which is how a spawn asks for silence from a job whose parent is being watched. A partial request is not merged into anything else; naming one channel settles the matter for all of them.Otherwise, the child inherits its parent’s output forwarding - unless the PRRTE
inheritMCA parameter was explicitly set to false. “Inherits” means the child is treated the way the parent is being treated: whoever is receiving the parent job’s output also receives the child’s, on the same channels and with the same formatting.
Two properties of that second rule matter as much as the rule itself, and they are what make this more than a local bookkeeping change:
It must hold regardless of which daemon the originating tool attached to when it launched the parent job, and it must hold under
prterunas well as under a persistent DVM.It must also hold for a tool that connects to a DVM daemon after a job is running and asks (through
PMIx_IOF_pull) for that job’s output to be forwarded to it. If that job subsequently spawns a child, the tool’s request carries over to the child, so the tool receives a copy of the child’s output too.
Finally, one thing that must not happen: output is never forwarded to an application process. Doing so is a loopback rather than a fallback - the output would simply be emitted again on the receiving process’s own stdout, for the runtime to collect and forward a second time. So where there is nothing to inherit, no forwarding is set up at all.
6.9.2. How output forwarding works today
Responsibility is split cleanly between the two projects, and keeping the split straight is the key to reading any of this code:
PRRTE gets the bytes to the right daemon. A
prtedcaptures its local processes’ output and forwards it to the HNP; the HNP fans it back out. There is no daemon-to-daemon traffic - everything funnels through the HNP. PRRTE’s own write machinery (sinks, write handlers) is used almost entirely for thestdindirection.PMIx decides who gets a copy, and emits it. Every daemon hands the output it has to
PMIx_server_IOF_deliver(). The PMIx server writes it locally if it is meant to, then walkspmix_globals.iof_requests- the list ofpmix_iof_req_tsubscriptions - and sends a copy to every registered requestor whose channel and source filters match. Output that matches nothing is cached inpmix_server_globals.iofuntil it ages out of that bounded cache.
A tool’s subscription is therefore held by the PMIx server of the daemon that tool attached to, and only that server can deliver to it. For output produced elsewhere to reach that tool, PRRTE must relay a copy to that daemon. That is what prte_iof_hnp_relay_to_tool() does, and it selects the destination from jdata->originator.
Two details of the current implementation are easy to miss and change the analysis:
The HNP hands everything it receives to its own PMIx server, not merely what it relays onward. So a tool attached to the HNP - the ordinary
prun --dvm-uricase - is reachable for any job’s output, whether or not the relay would have selected the HNP.PRRTE already inherits the parent’s output formatting directives onto a spawned child, gated on the same
inheritflag as mapping, ranking and binding: tagging, timestamping and stderr/stdout merging are copied from parent to child. What is not inherited is anything about who receives the output.
6.9.3. What is missing
PMIx did not give a spawned job any subscription at all. The spawn parser defaulted the output channels on only when the spawn came from a tool, so a spawn issued by an ordinary application process - an MPI_Comm_spawn - subscribed to nothing, and the child’s output matched no request and aged out of the cache unseen. Asking explicitly did not help either: PMIX_FWD_STDOUT is documented as forwarding the output “to this process”, and the request was registered and the bytes shipped, but every place that would arm local writing for the spawned namespace is gated on the peer being a tool. There was no combination of directives a client could pass that worked.
This part is fixed. A spawn that names no output channel now clones the subscriptions covering the spawning process’s own job onto the child’s namespace, keeping the requestor, channels, formatting and handler id, so the child’s output goes where the parent’s goes. See inherit_parent_iof() in src/server/pmix_server_iof.c. It is only sufficient when the spawn is processed on the same server that holds the parent’s subscription - a single-node DVM, prterun on one node, or any layout where the spawner’s daemon is also the tool’s daemon.
The inheritance is performed on the wrong daemon for a multi-node DVM. pmix_server_process_iof() runs on whichever PMIx server received the spawn command, which is the daemon hosting the spawning process. The tool’s subscription for the parent job lives on the daemon the tool attached to. When those differ, the clone is built where the output does not arrive, and the server that does have the output has nothing to match it against.
This is now demonstrated rather than argued: contrib/dockerswarm/run-spawn-iof.sh runs the same one-rank job twice against the same six-node DVM and the same tool, moving only the rank. With the rank on the tool’s own daemon the child’s output comes back on both channels; with the rank one node over, none of it does. The run also sharpens the diagnosis in a way worth keeping. In the failing case the child job happened to be placed on the tool’s own node, so its output reached the very server that holds the tool’s subscription - and was still dropped, because no subscription for the child’s namespace was ever created anywhere. The two halves of the gap are therefore independent: this one is a PMIx bug about where the inheritance is performed, and it would still bite after PRRTE learns to route a child’s output correctly.
PRRTE has no notion of a set of interested daemons. jdata->originator is a single pmix_proc_t - “originator of a dynamic spawn” - and prte_iof_hnp_relay_to_tool() relays a job’s output to exactly that one daemon. For a client-issued spawn the field is set to the requesting process and then overwritten at the HNP with the daemon that forwarded the launch request, so a child job’s output is relayed toward the spawner’s daemon rather than toward any tool.
A tool that pulls an existing job’s output is not recorded anywhere. The iof_pull upcall handler defines sinks on the daemon’s own stdout/stderr and returns; it does not record which tool asked, or on which daemon it sits. Consequently a tool attached to a daemon that hosts none of a job’s processes receives nothing from the other nodes - and this is true of the parent job, not merely of a child. The last of the desired properties above therefore cannot be layered on top of the current code; the parent case has to be built first.
6.9.4. The plan
The division follows the architectural split: PRRTE learns to route a job’s output to every daemon that has an interested tool, and PMIx learns to recognize that a subscription covering a parent job also covers its children.
Planned - PRRTE.
Give a job a set of interested daemons for output-routing purposes, rather than deriving a single destination from
jdata->originator. Seed it with the daemon hosting the tool that launched the job, which is whatoriginatorsupplies today.Record the requesting tool’s daemon into that set in the
iof_pullupcall, for each job the request names. This is what makes a tool that attaches later and pulls an existing job’s output work across nodes at all, and it is a prerequisite for the child case rather than part of it.Fan out in
prte_iof_hnp_relay_to_tool()to the whole set instead of to a single daemon.Initialize a spawned child’s set from its parent’s, subject to the
inheritrule. This is the routing half of the desired behavior, and it is what makes the property hold regardless of where the originating tool attached.
PMIx - done. Both items below are implemented.
A delivery-time ancestry fallback. When output for a namespace matches no subscription - the point at which the server used to fall through to caching it - the server walks that namespace’s
PMIX_PARENT_IDchain and tests the subscriptions against each ancestor. A daemon that receives a child’s output and holds a subscription for one of its ancestors delivers it, with no new wire protocol and no propagation of subscriptions between servers. Seeinherit_from_ancestry()insrc/server/pmix_server_iof.c.Two details of the implementation answered open questions that were on this page:
A match clones the ancestor’s subscriptions onto the child’s namespace rather than delivering that one chunk, so every later chunk takes the ordinary path and the datastore is consulted once per namespace instead of once per line. The clones are the same ones the spawn-time path creates, made by the same function, and have the same lifetime.
The walk is transitive, bounded by
PMIX_IOF_MAX_ANCESTRY. “Treated the way its parent is treated” says nothing about depth, and the chain is host-supplied data, so the bound is what keeps a malformed or circular one from spinning the progress thread. It stops at the first ancestor anybody here is watching: once the clones exist, that generation’s watchers are this namespace’s watchers.
This turned out to close more than it was expected to.
PMIX_PARENT_IDis per-process job data, so it is available on any server the namespace was registered with, and PRRTE funnels every daemon’s output through the HNP, which hands everything it receives to its own PMIx server - which is where an ordinaryprunorprteruntool’s subscription lives. So for a tool attached to the HNP, the fallback alone is sufficient:run-spawn-iof.shnow passes with the tool, the spawning rank and the child job on three different daemons, against unmodified PRRTE.A channel for the host’s inheritance decision:
PMIX_IOF_INHERIT, a boolean the host places in the job-level information of the spawned namespace. Absent means “inherit”, so a host says nothing unless it is turning inheritance off. Both decision points consult it throughiof_inherit_allowed().Job information is the channel rather than anything carried with the spawn request, and that follows from the two decision points being on different daemons: the flag has to be readable by the server the output arrives at, which may have had nothing to do with the spawn. Job info is exactly the thing PRRTE already ships to every daemon that registers the namespace - it is how
PMIX_PARENT_IDgets there. Nothing about it is read by the spawned processes; it is server-side state that happens to be keyed by their namespace. A server-wide default atPMIx_server_initwas rejected because it cannot express a per-job answer, and PRRTE’s inherit setting is per-job.The spawn-time site needs one more rule, because it is the only place where “the host said nothing” and “we cannot ask yet” are distinguishable. It runs from the spawn completion, and whether the child’s namespace has been registered with that server by then is the host’s business - a daemon hosting none of the child’s processes may never register it at all. A fetch that found nothing there would read as “inherit”, which is the wrong way to be wrong: it would clone against the host’s wishes and nothing would take the clone back. So an unregistered namespace defers to the delivery-time half, which cannot run before the namespace exists and therefore always has a real answer. Deferring is never behaviorally wrong - it only postpones a decision that the other half will make correctly.
The spawn-time inheritance is kept rather than replaced. It is what implements the precedence rule itself - the spawn request’s own attributes against the parent’s settings - and it answers the co-located case without waiting for output to arrive. The two are independent: neither needs the other to have happened, and where both apply the second finds the subscription the first created and does nothing.
The PRRTE side. Items 1-4 are the answer for a tool that is not attached to the HNP, and for a tool that attaches to a running DVM and pulls a job’s output. Neither is reachable from the PMIx side: the first needs the output relayed to a daemon PRRTE does not otherwise send it to, and the second needs the request recorded against the job at all. All four are implemented in prrte#2658, along with PRRTE’s half of item 6 - setting PMIX_IOF_INHERIT when its own inherit rule refuses. Note the default there is the opposite of this one: PRRTE does not inherit launch directives unless asked, so the gate on output forwarding has to be the explicit refusal rather than the resolved value.
6.9.5. Answered
Is a new attribute the right channel for the host’s inheritance decision? Yes, and it is
PMIX_IOF_INHERITin the spawned namespace’s job information. The alternative considered - PMIx having no opinion, and a spawn controlling its own output through thePMIX_FWD_*directives it already has - would have left a launcher’sinheritsetting silently not covering output forwarding. See item 6.Does
PMIX_PARENT_IDreach a child namespace’s registration on daemons that did not process the spawn? Yes. PRRTE adds it to the per-process job data inprte_pmix_server_register_nspace(), which every daemon registering the namespace receives;pmix_pfexecrecords it for the job as a whole. The lookup tries the specific rank and then the wildcard, so both shapes answer. Demonstrated byrun-spawn-iof.shcase 3, where the decision is taken on a daemon that hosts neither the spawner nor the child.Should the ancestry walk be transitive? Transitive, bounded. See item 5.
Should a match register a real subscription for the child, or be re-tested per delivery? Register. Re-testing looked simpler until the cost was written down: it puts a datastore fetch in front of every line a watched job writes, where registering pays once per namespace. The lifetime question it seemed to add is not a new one - the clone is identical to the one the spawn-time path already creates, and is made by the same function.
6.9.6. Sequencing and testing
The PMIx delivery-time fallback landed first, and it turned out to be sufficient on its own for a tool attached to the HNP - which is every tool PRRTE’s own launchers produce. The remaining PRRTE work is what a tool attached elsewhere needs.
None of this is reachable from make check. The spawn-time inheritance is covered by test/unit/iof_inherit.c, which drives the parser and the registration directly and reads the subscription list back - it establishes which subscriptions are created and for whom, which needs no spawn, and no spawn is available in any case because test/simple/simptest cannot host one. Everything described above as planned is about which daemon holds a subscription and where bytes are relayed, so it can only be demonstrated across several daemons. The ten-container swarm under contrib/dockerswarm is the vehicle for that, and the cases worth building there are:
a client-issued spawn where the spawning rank is hosted by a daemon other than the one the launching tool attached to;
the same under
prterunrather than a persistent DVM;a tool that attaches to a non-master daemon, pulls a running job’s output, and then observes the output of a job that job spawns.
The first two are built, in contrib/dockerswarm/run-spawn-iof.sh, driving examples/spawn_iof.c; README §21 describes the geometry. The runner reports:
spawner on the tool’s daemon - the child’s output comes back on both channels. This is the control: it says the example, the launcher and the capture work, so that the next line means something.
spawner one node over - nothing of the child’s output comes back. This is the reproducer, and it fails today.
the same under prterun - the output does reach the terminal.
prterunalso writes output locally, so its terminal is fed by a route that has nothing to do with the subscription being inherited; a pass here is not evidence that the inheritance worked, and the runner says so rather than counting it.
Reading a negative result needs one more thing, because “the forwarding dropped it” and “the child never ran” look identical from the tool and want opposite repairs. spawn_iof --markers <dir> therefore has every process drop a file of its identity on the node it is running on, by a channel with nothing to do with IOF; the runner gathers those from all ten nodes and prints the layout under every case. That is how the failing case above can say where the child ran.
The third case is built as well, as case 5, driving examples/iof_watcher.c - a tool that attaches to whatever server is local to the node it is started on, and so lands on a non-HNP daemon when started anywhere but node1. It is the only case that depends on a tool’s iof_pull being recorded against a job at all, and it therefore fails against a PRRTE without item 2. Its ordering is load-bearing: the watcher has to be subscribed before the child exists, or a pass would only show cached output being replayed, so spawn_iof --delay holds the spawn open and both asserted lines are written after the subscription.