Debugging performance regressions
MPI software stacks are complex beasts that play a key role in application performance. Guix packages Open MPI for a wide range high-speed interconnects—InfiniBand, Omni-Path, Slingshot, ROCE, you name it—with the goal of achieving peak performance for each one of them.
The key challenge from a packaging perspective is to ensure no performance regression occur for any of these network types when Open MPI or its dependencies change. Administrators of clusters where Guix is the main deployment tool typically have checks in place for this. But what if a regression does occur?
This blog post is a report from the trenches, showing how a performance regression of the MPI stack on Slingshot interconnects was identified. The debugging story is in itself a showcase for the control and transparency that Guix affords.
The plot
Slingshot support is relatively recent in Open MPI as packaged by Guix and, until now, concerned a relatively small part of Guix usage in HPC. No automated performance regression testing was in place for this interconnect, primarily due to difficulties accessing hardware that would allow us to do that. And so, the inevitable happened: a performance regression struck.
The symptom is simple: running an MPI bandwidth/latency benchmark would
show poor performance (here we force the use of version 5 of Open MPI
with
--with-input):
$ guix shell intel-mpi-benchmarks --with-input=openmpi=openmpi@5 openmpi@5 -- \
mpirun IMB-MPI1 PingPong
…
# PingPong
#---------------------------------------------------
# Benchmarking PingPong
# #processes = 2
#---------------------------------------------------
#bytes #repetitions t[usec] Mbytes/sec
0 1000 37.94 0.00
1 1000 38.30 0.03
2 1000 38.41 0.05
…
1048576 40 575.85 1820.92
2097152 20 983.58 2132.15
4194304 10 1817.18 2308.15Uh-oh—peak bandwidth is 2.3 GB/s instead the expected 25 GB/s or so.
The reason this happens is because libcxi (the low-level Slingshot support library) errors out, and then libfabric (which sits right above libcxi) propagates that error to Open MPI, which in turn falls back to plain TCP/IP over Ethernet.
We can get debugging info from Open MPI, libfabric, and libcxi by passing the right flags and environment variables:
$ FI_LOG_LEVEL=9 guix shell intel-mpi-benchmarks --with-input=openmpi=openmpi@5 openmpi@5 -- mpirun --mca btl_ofi_verbose 3 --mca pml_ucx_verbose 3 --mca btl ofi --mca mtl ofi --mca btl_base_verbose 100 IMB-MPI1 PingPong
[c1510:1386001] mca: base: components_register: registering framework btl components
[c1510:1386001] mca: base: components_register: found loaded component ofi
[c1510:1386001] mca: base: components_register: component ofi register function successful
…
[c1510:1386001] mtl_ofi_component.c:343: mtl:ofi:provider: cxi
[c1510:1386001] mtl_ofi_component.c:368: mtl:ofi:provider:domain: cxi0
libfabric:1386001:1781018439::cxi:domain:cxip_domain_enable():457<warn> c1510: Security Issue: Using unrestricted service ID 1 for cxi0. Please provide a service ID via auth_key fields.
libfabric:1386001:1781018439::cxi:domain:cxip_cp_get():109<warn> c1510: Failed to allocate CP, ret: -22 VNI: 1 TC: BEST_EFFORT TYPE: DEFAULT
libfabric:1386001:1781018439::cxi:domain:cxip_cp_get():112<warn> c1510: Remapping original TC from BEST_EFFORT to BEST_EFFORT
libfabric:1386001:1781018439::cxi:domain:cxip_cp_get():137<warn> c1510: Failed to allocate CP, ret: -22 VNI: 1 TC: BEST_EFFORT TYPE: DEFAULT
libfabric:1386001:1781018439::cxi:domain:cxip_cmdq_alloc():289<warn> c1510: Failed to allocate CP: -22
libfabric:1386001:1781018439::cxi:ep_ctrl:cxip_ep_cmdq():95<warn> c1510: Unable to allocate CMDQ, ret: -22
libfabric:1386001:1781018439::cxi:ep_ctrl:cxip_ep_ctrl_init():567<warn> c1510: Failed to allocate control TXQ, ret: -28
libfabric:1386001:1781018439::cxi:ep_ctrl:cxip_ep_enable():691<warn> c1510: cxip_ep_ctrl_init returned: -262
[c1510:1386001] mtl_ofi_component.c:1093:fi_enable failed: Invalid resource domain
[c1510:1386001] select: initializing btl component ofi
[c1510:1386001] btl_ofi_component.c:318: btl:ofi:provider_include = "(null)"
[c1510:1386001] btl_ofi_component.c:320: btl:ofi:provider_exclude = "shm,sockets,tcp,udp,rstream,usnic,net"
libfabric:1386001:1781018439::core:core:fi_getinfo_():1391<warn> fi_getinfo: provider efa output empty listThe main clue here are the “Failed to allocate CP” errors that originate in the Slingshot “provider” of libfabric (CP stands for communication profile in libcxi).
Bisecting the distro
Where do we go from here? Short of being a libcxi expert, a good way forward is to find out what change introduced that regression. There are two good news here: first the MPI stack provided by Guix is entirely self-contained—so software on the machine that is not provided by Guix is unlikely to be the culprit—, and second we knew of a known-good state.
More precisely, we knew the exact commit of Guix that provided a
“good” MPI stack for Slingshot; and we knew of a commit that provided a
“bad” MPI stack. It is worth stressing this: the Guix commit shown by
guix describe is all it takes to identify the entire MPI
stack—package versions, configure flags, compilers, etc. From a Guix
commit, we can redeploy that stack any time with guix time-machine.
Naturally, we bisected Guix itself in search of the offending commit. At each step, we allocate two nodes and run:
guix time-machine -q --commit=THE-COMMIT -- \
shell intel-mpi-benchmarks --with-input=openmpi=openmpi@5 openmpi@5 -- \
mpirun IMB-MPI1 PingPongIn a matter of minutes, we’d be able to categorize the commit being tested as “good” or “bad”. (Automating categorization by parsing the output of the benchmark would have been nice but in this case the cost/benefit ratio did not look favorable.)
The offending commit turned out to be an innocent-looking libcxi upgrade.
Finer-grain bisection
Why stop at the distro level when we could pinpoint the libcxi commit
that introduces the regression? While time-machine allows us to test
specific changes in the distro, the --with-commit
option
allows us to rebuild an entire stack with a specific commit of one of
its packages—keeping the rest of the stack unchanged.
Using this feature we were able to continue our bug hunt, this time bisecting at the level of individual libcxi commits, starting with the known-good commit (corresponding to libcxi version 13.0.0) up to the first known-bad commit (libcxi version 13.1.0):
guix shell intel-mpi-benchmarks --with-input=openmpi=openmpi@5 openmpi@5 \
--with-commit=libcxi=THE-COMMIT -- \
mpirun IMB-MPI1 PingPongThis bisection is a bit more expensive than the previous one because at each step, most of the MPI stack needs to be rebuilt (specifically libcxi and its dependents: libfabric and Open MPI). Still, after a few hours, the offending commit was identified, which allowed us to provide a detailed bug report for the libcxi developers (the bug is still opened as of this writing).
Conclusion
The reason I write about this anecdote is because I think it
demonstrates a kind of control over the software stack that is
unthinkable without Guix or its sibling Nix. With guix time-machine
we were able to bisect the entire distro until we could find the
commit that introduced the regression; using --with-commit we
continued that investigation at the level of a single package.
There is currently no guix bisect command, but it sure sounds like it
would be a useful one to have, as this story demonstrates.
As for tracking MPI stack performance, we are looking forward to setting up a publicly accessible dashboard tracking Open MPI performance on supported interconnects using the Intel or OSU micro-benchmarks. Stay tuned!
Acknowledgments
Many thanks to the colleagues at CINES who provided guidance on the initial investigation of this performance regression.
Unless otherwise stated, blog posts on this site are copyrighted by their respective authors and published under the terms of the CC-BY-SA 4.0 license and those of the GNU Free Documentation License (version 1.3 or later, with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts).





