Skip to content

Performance: hierarchical barrier, tiered huge pages, and OFI/CXI transport improvements#1237

Draft
bcmIntc wants to merge 14 commits into
Sandia-OpenSHMEM:mainfrom
bcmIntc:bcm_Performance
Draft

Performance: hierarchical barrier, tiered huge pages, and OFI/CXI transport improvements#1237
bcmIntc wants to merge 14 commits into
Sandia-OpenSHMEM:mainfrom
bcmIntc:bcm_Performance

Conversation

@bcmIntc

@bcmIntc bcmIntc commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Adds a NIC-offloading hierarchical barrier and reintroduces two CXI/heap optimizations in isolation, each independently gated.

Hierarchical barrier (XPMEM + NIC dissemination)

  • New --enable-hierarchical-barrier (98af1b7): three-phase barrier — intranode gather/fanout via CPU atomics over XPMEM (cache-line-padded per-PE slots), NIC puts restricted to the internode dissemination phase (node roots only). Keeps intranode traffic off the NIC.
  • Correctness + scratch fixes (dbe498c, 71521f4): per-team sense counters eliminate the overlapping-team signal-mismatch deadlock; persistent init-time scratch replaces the alloca/malloc-per-call; node-id-based root sets; dead-code cleanup.
  • Per-team topology caching (33cc4be): active-set/root-set/tree topology is invariant per team but was rebuilt every call (incl. an O(PE_size²) scan). Cache it per team, build once; root-set build made O(PE_size). Allocation-failure-safe fallback to scratch.
  • AUTO-selection gating (d5b40ee): only select hierarchical when the job spans multiple nodes and on a job-global min PPN, so every PE makes the same choice on heterogeneous-PPN jobs. Explicit BARRIER_ALGORITHM=hierarchical still honored.
  • --enable-hierarchical-barrier now requires XPMEM specifically (intranode phase needs shmem_shr_transport_ptr).

CXI hybrid MR descriptor (9eab54d, 3ba9445, 2cc39b2)

Enables FI_CXI_DOM_OPS_3 enable_hybrid_mr_desc after fi_domain() (as Cray SHMEM does) so the provider trusts a non-NULL desc and skips its internal MR-registration walk on large puts. Gated by SHMEM_OFI_CXI_HYBRID_MR_DESC and an exact-strcmp provider check. Compiled in only when a configure probe finds the real struct fi_cxi_dom_ops member (no hand-rolled struct mirror → no ABI-drift risk); the probe supplies a full prerequisite-include set so it's robust across libfabric versions/clusters, with a configure-summary line surfacing the result.

Symmetric heap huge pages (5285889)

Reintroduces tiered huge-page allocation gated on SHMEM_SYMMETRIC_HEAP_USE_HUGE_PAGES (default off): hugetlbfs file → anonymous MAP_HUGETLB → regular pages. No THP/madvise tier. Fallback path is byte-for-byte the prior fixed-address mapping, preserving VA symmetry across PEs.

Misc #1236

  • db8cf24: zero max_buffered_send before the fi_getinfo hint so the provider advertises its natural inject_size.

Notes:

CXI hybrid MR and huge pages are both off/neutral by default. All optimizations are individually gated for safe A/B testing.

markbrown314 and others added 6 commits February 24, 2026 12:36
Do not force hard polling when XPMEM is enabled.
This conflicts with counter based polling when used with OFI transport.

Issue Sandia-OpenSHMEM#1217

Signed-off-by: Mark F. Brown <mark.f.brown@intel.com>
Replaced complex polling with simpler OFI counter wait

Issue Sandia-OpenSHMEM#1217

Signed-off-by: Mark F. Brown <mark.f.brown@intel.com>
Issue Sandia-OpenSHMEM#1221

Signed-off-by: Mark F. Brown <mark.f.brown@intel.com>
Let the provider advertise its natural inject_size rather than
requiring it to be at least sizeof(long double). The returned
inject_size is adopted immediately after fi_getinfo.
Adds --enable-hierarchical-barrier, a three-phase barrier that keeps
intranode traffic off the NIC by using CPU atomics over XPMEM for
gather/fanout and restricts NIC puts to the internode phase (node roots
only).

Phase 1 (intranode gather): local PEs signal up a k-ary tree. Each PE
writes to its OWN up-slot in local_pSync; the parent reads each child's
slot individually. Slots are padded to one cache line (HIER_SLOT_STRIDE=8
longs, 64 bytes) so no two PEs share a line, eliminating the MESI
serialization that would occur if all children wrote to a single counter.
Signal values increment monotonically via hier_sense, avoiding explicit
slot resets between calls (sense alternation).

Phase 2 (internode dissemination): node roots run a put-based binary
dissemination across the NIC. After each round the slot is reset via a
CPU store rather than a self-put, saving ceil(log2(N_nodes)) NIC
round-trips per barrier (12 at 4096 nodes).

Phase 3 (intranode fanout): node root CPU-stores an ack into each child's
down-slot; children relay down the k-ary tree with reset-before-signal
ordering. Down-slots are in the upper half of local_pSync, laid out with
the same per-PE cache-line padding as up-slots.

AUTO selection activates when local PE count >= SHMEM_HIER_BARRIER_THRESHOLD
(default 2). Also selectable via SHMEM_BARRIER_ALGORITHM=hierarchical.

New infrastructure:
- src/shr_transport.h  — XPMEM CPU pointer mapping; self-access returns
  the address directly without an XPMEM lookup
- src/runtime_util.c  — global hostname exchange so each PE can identify
  its node root
- configure.ac  — --enable-hierarchical-barrier requires --with-xpmem and
  a network transport
@bcmIntc bcmIntc self-assigned this Jun 25, 2026
bcmIntc added 6 commits June 25, 2026 12:04
…archical barrier

Three coupled refinements to the hierarchical barrier:

Per-team sense state. A static global hier_sense caused a signal
mismatch when PEs participated in different barrier teams: only PEs in
the active set incremented it, so PEs that skipped a subset barrier
carried a stale sense value into the next TEAM_WORLD barrier. Move
hier_sense into shmem_internal_team_t; TEAM_WORLD barriers
(PE_start=0, PE_stride=1, PE_size=num_pes) use team-local state, and
subset barriers use a static fallback. All PEs in TEAM_WORLD now stay
synchronized across interleaved team/subset barrier sequences.

Persistent active-set scratch. The barrier built its local_pes/root_pes
arrays fresh on every call. An alloca-per-call risked stack overflow at
scale (PE_size up to 100K+ is ~400KB per array), while malloc/free
per-call serialized the hot path and tanked throughput. Allocate both
once in shmem_internal_collectives_init, sized to num_pes, and reuse.
Safe to share across calls because barriers are serialized -- a PE
cannot enter a new barrier until it has exited the previous one, the
same invariant the shared hierarchical_local_psync relies on.

Dead-code removal. Drop three unused symbols that produced -Wunused
warnings: shmem_internal_cpu_atomic_load_long (never called),
tree_parent_shr (computed but never read -- the Phase 1 up signal writes
to the PE's own up-slot, which the parent reads directly), and
my_up_slot (only the down-slot is read in the spin loops; my_up_raw is
still used for the parent store).
Cray SHMEM calls FI_CXI_DOM_OPS_3 enable_hybrid_mr_desc right after
fi_domain(); SOS does not.  Without it, the CXI provider performs
internal memory registration on every fi_write/fi_writemsg/fi_fetch_
atomicmsg call where the desc field is non-NULL, even when the buffer
already lives in a provider-registered region.  Each MR cache lookup
adds latency to large RDMA puts.

With hybrid mode enabled, the provider trusts a non-NULL desc and skips
its internal registration walk, dropping per-call overhead on the put
data path.  The call must occur before any endpoints are created; the
domain only propagates the setting to child endpoints at creation time.

Reintroduces the provider-name infrastructure to gate this: the
provider name is captured via strdup in query_for_fabric (it must
outlive fi_freeinfo) and freed in shmem_transport_fini.  An inline
shmem_transport_ofi_check_provider helper does an exact strcmp so that
the call is attempted only on a plain cxi provider and layered
providers do not inherit it.

The dom_ops struct fields are mirrored locally so the build does not
require rdma/fi_cxi_ext.h.  Gated by SHMEM_OFI_CXI_HYBRID_MR_DESC=true
(default) for A/B testing without rebuilding.
Reintroduce the symmetric heap huge page allocator gated on
SHMEM_SYMMETRIC_HEAP_USE_HUGE_PAGES (default off).  When enabled, the
heap is backed by explicitly reserved huge pages via two paths, tried
in order:

  1. hugetlbfs file mapping (when a matching hugetlbfs mount exists)
  2. anonymous MAP_HUGETLB with an explicit 2MB page size (works with
     nr_overcommit_hugepages)

When neither reserved-huge-page path succeeds, the heap falls back to
regular pages.  There is intentionally no transparent-huge-page
(madvise) tier: only explicitly reserved huge pages are used, otherwise
regular pages.

The regular-page fallback is a single fixed-address mmap at
requested_base, identical to the mapping used when huge pages are
disabled, so the symmetric heap keeps the same virtual address on every
PE whether or not the huge-page flag is set.

mmap_alloc now reports the actual mapped size back to the caller, since
the hugetlbfs path rounds the length up to a huge-page boundary; that
rounded size is used for munmap and transport registration.

Disabled by default, so the regular-page path is byte-for-byte the
prior behavior unless the flag is set.
…chical barrier

Give the hierarchical barrier a per-team sense counter to eliminate the
overlapping-team deadlock. shmem_internal_sync_hierarchical gains a
hier_sense_ptr parameter: non-NULL supplies a caller-owned per-team
counter; NULL retains the prior selection (TEAM_WORLD uses its own
counter, other active sets share a static fallback). New
shmem_internal_sync_for_team / shmem_internal_barrier_for_team wrappers
pass &team->hier_sense, and shmem_team_sync, the team psync-recycle
barrier, and the two team-split barriers route through them. sync_all /
barrier_all pass &team_world.hier_sense explicitly.

Add shmem_internal_sync_no_hier for the deprecated shmem_barrier /
shmem_sync APIs: they carry no team context, so they never select the
hierarchical algorithm and fall through to linear/tree by COLL_CROSSOVER.

Build root active sets from a global node_id array instead of
is_node_root. shmem_runtime_get_node_id(pe) returns the global PE number
of the lowest-ranked PE on pe's node, computed once at init across all
runtimes (mpi, pmi, pmi2, pmix). The root representative for each node is
the lowest-ranked active PE on it (local_pes[0]), and the degenerate-case
guard is reduced to local_count == PE_size.
Each hierarchical barrier rebuilt the active-set topology from scratch:
the local PE set, the per-node root set, virtual indices, tree children
(in shr-rank space), and the phase-2 dissemination round count. For a
fixed team this is all invariant, yet it ran on every call -- including
an O(PE_size^2) root-set scan, an O(PE_size) local scan, and a
shmem_runtime_get_node_rank lookup per tree child.

Cache it per team. Add shmem_internal_hier_cache_t to the team struct,
built once on the first barrier (shmem_internal_hier_compute) and reused
thereafter. shmem_internal_sync_hierarchical takes a hier_cache pointer:
team-aware callers (sync_all, barrier_all, sync_for_team) pass
&team->hier_cache; callers with no team context pass NULL and build into
the shared per-call scratch as before. After the first call, per-barrier
topology cost drops to a few field loads.

Only topology is cached, not XPMEM mapped pointers: those depend on the
local_pSync array (sync_all and barrier_all share a team but pass
different arrays) and are cheap base+offset arithmetic to re-derive.

Also make build_root_active_set O(PE_size) via a node_id-keyed seen-flag
buffer (hier_node_seen) instead of the nested O(PE_size^2) scan, so even
the one-time first-barrier build is linear at scale. The buffer is
self-cleaning (resets only touched entries) to avoid an O(PE_size) clear
per call.

Cache persistence is allocation-failure safe: if any of the three
exact-size buffers fails to allocate, the barrier falls back to the
scratch topology for that call without persisting (valid stays 0), so it
still completes correctly and retries the build next time.

The topology cache is zeroed for the three predefined teams (world,
shared, node; split teams are calloc'd) and freed in
shmem_internal_team_destroy.

configure.ac: --enable-hierarchical-barrier now requires XPMEM
specifically, not "XPMEM or CMA". The intranode phase does CPU atomics on
peers' slots through shmem_shr_transport_ptr, which only XPMEM provides;
a CMA-only build would have configured successfully then failed at the
first barrier.
… min PPN

AUTO barrier selection chose the hierarchical algorithm whenever the
local node had >= HIER_BARRIER_THRESHOLD PEs. Two problems:

1. On a single-node job the hierarchical barrier runs its degenerate
   intranode-only path, which is strictly worse than linear/tree: under
   USE_HIERARCHICAL_BARRIER the shr transport already routes single-node
   atomics through CPU atomics (shmem_shr_transport_use_atomic), so
   linear/tree run over shared memory with none of the hierarchical
   scaffolding. Now require the job to span more than one node
   (shr_size < num_pes) before AUTO selects hierarchical.

2. The threshold was checked against the local node size, which differs
   per PE on a heterogeneous-PPN job -- a large node could select
   hierarchical while a small node selects tree, diverging within one
   collective. Gate instead on a job-global minimum PEs-per-node,
   computed once in collectives_init from the global node_id array
   (already exchanged at init, no new communication): tally PEs per node,
   take the min over populated nodes. Both terms of the predicate are now
   job-global, so every PE makes the same selection.

Consolidated into shmem_internal_hier_barrier_available(), used at all
AUTO selection sites (sync, sync_all, barrier_all, sync_for_team) and the
init-time barrier-algorithm diagnostic. Explicit
BARRIER_ALGORITHM=hierarchical is unaffected and always honored.
@bcmIntc
bcmIntc force-pushed the bcm_Performance branch 2 times, most recently from a317318 to eb627bf Compare June 26, 2026 11:56
configure checks for rdma/fi_cxi_ext.h and, when present, probes struct
fi_cxi_dom_ops for the enable_hybrid_mr_desc member (added in a later
libfabric).  The hybrid local MR descriptor optimization is compiled in
only when both HAVE_RDMA_FI_CXI_EXT_H and
HAVE_STRUCT_FI_CXI_DOM_OPS_ENABLE_HYBRID_MR_DESC are defined, using the
provider's real struct fi_cxi_dom_ops and the FI_CXI_DOM_OPS_3 macro.

There is no hand-rolled struct mirror, so the build never risks an ABI
mismatch against the provider's actual dom_ops layout.  When support is
absent and the runtime requests hybrid mode, PE 0 warns once and the
request is ignored.
@bcmIntc

bcmIntc commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Test config:

--with-ofi=/opt/cray/libfabric/1.22.0/ --with-xpmem=/usr/lib --enable-pmi-mpi --disable-fortran --enable-ofi-mr=scalable --enable-ofi-manual-progress --disable-libtool-wrapper --enable-mr-endpoint --enable-manual-progress --enable-hierarchical-barrier CC=mpicc CXX=mpicxx

Envt:

export SHMEM_SYMMETRIC_HEAP_USE_HUGE_PAGES=1
export SHMEM_SYMMETRIC_SIZE=3G
export SHMEM_BARRIER_ALGORITHM=auto
export SHMEM_OFI_PROVIDER=cxi
export FI_PROVIDER=cxi
export FI_MR_CACHE_MAX_COUNT=32768
export FI_MR_CACHE_MAX_SIZE=-1
export SHMEM_BOUNCE_SIZE=8192

fi_cxi_ext.h references struct fid / uint64_t / bool / struct timespec
without including the headers that define them.  Whether those resolve
during the configure probe depends on what a given libfabric's fabric.h
pulls in transitively, which varies across clusters and versions, so the
header could be present yet reported as unusable and silently disable the
hybrid MR descriptor optimization.

Supply the full prerequisite set (stdbool.h, time.h, stdint.h,
rdma/fabric.h, rdma/fi_domain.h) in the AC_CHECK_HEADERS/AC_CHECK_MEMBER
test bodies so the probe compiles independently of fabric.h's transitive
includes.  Also report CXI hybrid MR desc support in the configure
summary so a misdetection is visible at configure time rather than only
as a runtime warning on CXI hardware.

Also lower the huge-page snprintf-failure message in symmetric_heap_c.c
from a warning to a debug message, matching the other fallback-path
messages in mmap_alloc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants