Skip to content

CORE: Add team cache with exact reuse and agreement vote - #1348

Draft
bwestheimer wants to merge 16 commits into
openucx:masterfrom
bwestheimer:bwestheimer/pub-team-cache-core
Draft

bwestheimer wants to merge 16 commits into
openucx:masterfrom
bwestheimer:bwestheimer/pub-team-cache-core

Conversation

@bwestheimer

Copy link
Copy Markdown
Collaborator

What

Add a per-context communicator team cache. Teams that are destroyed are retained as DORMANT rather than freed, and re-adopted on the next ucc_team_create_post with identical membership, skipping the ADDR_EXCHANGE and topology build that a fresh create would require.

A cross-rank BAND allreduce agreement vote runs on every cacheable create so all members reach the same reuse-or-build decision before any rank skips the exchange. This makes reuse safe when subcommunicators overlap. The cache is off by default (UCC_TEAM_CACHE_ENABLE=n).

Seven commits, intended to be read in order:

  1. Team cache identity fingerprint (FNV-1a hash + exact-compare, no container yet)
  2. Cache container, refcount API, and FIFO eviction
  3. Wire cache into context config and team lifecycle (first end-to-end working cache; AGREEMENT=n is a supported config, user guide documents the overlapping-scope restriction)
  4. Tests: dormant reuse lifecycle and cache knobs
  5. Cross-rank agreement vote
  6. Tests: agreement vote and overlap safety (includes a test that deadlocks with the vote disabled and passes with it enabled)
  7. CI: cache-on vs cache-off equivalence pass

Why

Team creation is the dominant latency in workloads that repeatedly create subcommunicators, such as split-collectives and windowed pipelines. Caching avoids the per-create allgather and topology build without changing the UCC API.

How

New files: src/core/ucc_team_cache.{c,h}. Modifications to ucc_team.{c,h}, ucc_context.{c,h}, and ucc_service_coll.{c,h} for lifecycle hooks and config. No TL or CL changes in this PR.

Depends on #1346, #1347.

@svcnbu-swx-hpcx

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agent — Lint (ROCm) · commit 6334deb2

TL;DR: The "Lint (ROCm)" clang-tidy static-analysis stage failed with real use-after-free defects (treated as errors) in the new team-cache code; the fix is to log the pointer before freeing it (and not use freed team memory in the debug log).

Full analysis

Summary: GitHub Actions "Lint (ROCm)" job failed (exit 125) because clang-tidy-17 flagged use-after-free / garbage-value defects as errors in src/core/ucc_team_cache.c and src/core/ucc_team.c, both new team-cache code from this PR.

Root cause: clang-analyzer found two genuine use-after-free bugs:

  • src/core/ucc_team_cache.c:95-96 — ucc_free(cache) frees the cache, then ucc_debug("ucc_team_cache destroyed: %p", cache_ptr) dereferences/reads cache_ptr (an alias of the just-freed cache) when the DEBUG log level is enabled → clang-analyzer-unix.Malloc use-after-free.
  • src/core/ucc_team.c:1052/1057/1065 — inside the pending_destroy/work loop, ucc_team_destroy_single(team) frees team (via ucc_team_destroy_single_ex → ucc_free(team) at ucc_team.c:907). The subsequent ucc_debug(... team_ptr ...) reads memory tied to the freed team, and the ucc_list_for_each_safe iterator over &work triggers a "garbage value / left operand of '-'" report. Note team_id/team_ptr are captured before the free, but the analyzer still reports the freed-memory access in the debug path and the list-iteration.

These are lint errors, not test failures — warnings treated as errors (3 in ucc_team.c, 1 in ucc_team_cache.c) caused the non-zero exit.

Implicated commit: 8e8001c0 "CORE: Add team cache container and FIFO eviction" (Bryce Westheimer) for ucc_team_cache.c; the pending_destroy eviction loop in ucc_team.c is from the same PR (#1348) series (830d5ad5 / 8e8001c0).

File: src/core/ucc_team_cache.c:95-96 and src/core/ucc_team.c:1052-1070

Suggested fix:

  • In ucc_team_cache_destroy: move the ucc_debug(...) call before ucc_free(cache) (log the pointer value first, then free). cache_ptr is captured but the analyzer still models the read as post-free because the debug expands after the free — reorder so free is the last statement.
  • In ucc_team.c eviction loop: the ucc_debug uses team_ptr/team_id which are already copied before ucc_team_destroy_single; keep those local copies but ensure the debug log does not touch team directly, and consider annotating the safe-iteration or restructuring so the analyzer doesn't see the freed team re-read. The cleanest is to emit the success ucc_debug using only the pre-saved scalars (already done) and verify no team-> field is referenced after the destroy — if the analyzer still flags the ucc_list_for_each_safe over &work, delete the node from work before calling ucc_team_destroy_single (it currently does at 1056, good) and use a plain while (!ucc_list_is_empty(&work)) pop loop instead of for_each_safe to avoid the tmp container-of on a possibly-freed element.

Related: PR #1348 (branch bwestheimer/pub-team-cache-core); commits 8e8001c0, 830d5ad5, db69bc5f.

@svcnbu-swx-hpcx

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agent — Linter-NVIDIA · commit 6334deb2

TL;DR: The Linter-NVIDIA clang-tidy job failed with 3 static-analyzer errors (use-after-free / garbage value) in core/ucc_team.c's new ucc_team_cache_progress_pending(), because ucc_team_destroy_single() calls ucc_free(team) and the caller then still touches the freed team. Fix by saving needed fields (or not freeing team on the terminal path) so no freed pointer is re-used or re-queued.

Full analysis

Summary: GitHub Actions Linter-NVIDIA (clang-tidy-17) failed — clang-analyzer-unix.Malloc (use-after-free ×2) and clang-analyzer-core.UndefinedBinaryOperatorResult (garbage value) in src/core/ucc_team.c, exit code 125 ("3 warnings treated as errors").

Root cause: ucc_team_destroy_single() → ucc_team_destroy_single_ex() unconditionally does ucc_free(team) (line 907) and returns UCC_OK. In the new ucc_team_cache_progress_pending() loop, after status = ucc_team_destroy_single(team) (line 1057) the freed team node is still used — the UCC_INPROGRESS branch re-adds &team->cache_link to cache->pending_destroy (line 1060) and the analyzer flags dereference of freed memory reachable through the loop/ucc_debug. The work "argument to free() is the address of a local variable" and "garbage value" errors are the analyzer conflating the stack-local work list head with the freed team node across this path. This is brand-new code from the team-cache feature on this branch.

Implicated commit: 51652723 "CORE: Wire team cache into team lifecycle" (Bryce Westheimer) — with 830d5ad5 on top; both introduce ucc_team_cache_progress_pending / the destroy-and-requeue loop.

File: src/core/ucc_team.c:1057 (destroy), :1060 (requeue of freed node), :1065 (debug); frees at src/core/ucc_team.c:905 and :907.

Suggested fix: Make the caller not touch team after a terminal destroy. Concretely: ucc_team_destroy_single only frees team when it returns UCC_OK; on UCC_INPROGRESS the team must stay live for re-queue. Restructure so the list-link manipulation happens on a non-freed object:

  • Remove the node from work before calling destroy (already done at line 1056), and only re-add to pending_destroy when status == UCC_INPROGRESS and the team was not freed. Ensure ucc_team_destroy_single_ex does NOT free team when returning UCC_INPROGRESS (verify the in-progress return path skips ucc_free(team)).
  • Since team_ptr/team_id are already cached before the destroy call (lines 1053–1054), the ucc_debug at 1065 should use only those cached values — confirm no field of the freed team is read there.
  • If the analyzer still can't prove the UCC_INPROGRESS path leaves team valid, add a clear invariant (e.g. a bool freed out-param from destroy) so the pending_destroy re-queue only runs on live memory, silencing the unix.Malloc diagnostic legitimately rather than with a NOLINT.

Related: PR #1348 (bwestheimer/pub-team-cache-core); commits 51652723, 830d5ad5. No pre-existing issue found in the tracker.

@svcnbu-swx-hpcx

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agent — Build & Test · commit 6334deb2

TL;DR: The Build & Test GHA job failed to compile because core/ucc_team_cache.c triggers GCC's -Werror=use-after-free: it logs cache_ptr (a copy of the freed cache pointer) after ucc_free(cache). Format the pointer value into a local variable before the free, or log before freeing.

Full analysis

Summary: Compilation of src/core/ucc_team_cache.c failed with -Werror=use-after-free, aborting make install in the build step.

Root cause: In ucc_team_cache_destroy(), line 95 calls ucc_free(cache) and then line 96 does ucc_debug("ucc_team_cache destroyed: %p", cache_ptr);. Since cache_ptr holds the value of the just-freed cache pointer, GCC 12+ flags passing that dangling pointer through the ucc_debug→ucc_log_dispatch path as a use-after-free. Because the build uses -Wall -Werror (and "all warnings being treated as errors"), this warning becomes a hard error and fails the build. The evasion attempt via the cache_ptr copy does not fool GCC — it tracks the pointer value.

Implicated commit: This file was introduced by Bryce Westheimer (PR #1348, branch bwestheimer/pub-team-cache-core); the relevant commits are 830d5ad5, 8e8001c0, db69bc5f (all authored 2026-08-13). The merge commit under test is fa5073a (PR head [REDACTED:Hex High Entropy String]).

File: src/core/ucc_team_cache.c:96 (with the offending free at line 95)

Suggested fix: Don't reference the freed pointer after freeing. Either move the debug log before the free, or capture the address as an integer first. Simplest:

    kh_destroy(ucc_team_cache_map, (ucc_team_cache_map_t *)cache->table);
    ucc_spinlock_destroy(&cache->lock);
    ucc_debug("ucc_team_cache destroyed: %p", (void *)cache);
    ucc_free(cache);
}

(and remove the now-unused cache_ptr variable). If the log must stay after the free, format the value into a uintptr_t/local before freeing: uintptr_t addr = (uintptr_t)cache; ... ucc_free(cache); ucc_debug("... 0x%" PRIxPTR, addr);.

Related: none found (introduced in this PR #1348).

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id f196e724-b0f3-450a-8b55-089b217dde1c in the triage console for the audit trail.

@bwestheimer bwestheimer changed the title CORE/TEAM_CACHE: Add communicator team cache with exact reuse and agreement vote CORE: Add team cache with exact reuse and agreement vote Sep 2, 2026
@bwestheimer
bwestheimer force-pushed the bwestheimer/pub-team-cache-core branch from 6334deb to 160a8f2 Compare September 2, 2026 20:41
@bwestheimer
bwestheimer force-pushed the bwestheimer/pub-team-cache-core branch from 160a8f2 to a54b9ae Compare September 3, 2026 14:04
Add an 'embedded' flag to ucc_service_coll_req_t. When set,
ucc_service_coll_finalize skips ucc_free(req) so the request can live in
caller-owned storage instead of being heap allocated. The team agreement
vote embeds its request directly in ucc_team_t and relies on this.

Add ucc_service_allreduce_ctx, which runs an allreduce over a
pre-materialized subset of context endpoints using the context-level
service team. The existing ucc_service_allreduce resolves subset ranks
through team->ctx_map, which is not yet built while a team is being
created. The new entry point maps subset indices straight to context
ranks and routes through ctx->service_team, so the agreement vote can run
before ctx_map exists.
Introduce ucc_team_cache_identity_t, the normalized key type used to
recognize a team by its membership, together with its build, compare and
free helpers and the cacheability policy check. No cache container is
added here.

ucc_team_cache_identity_build materializes members[] by evaluating the
ep_map, so an identity never aliases caller-owned storage such as an
ep_map callback closure or a user array. Any ep_map style (CB, ARRAY,
STRIDED, FULL) describing the same membership yields the same identity.

The FNV-1a hash covers membership only (size, self_ep, members[]).
ext_id is compared separately and deliberately not hashed, so teams with
the same membership but different external communicator ids land in the
same bucket; PR3a introduces the chaining that lets them coexist there.
ucc_team_cache_identity_equal_membership provides the ext_id-agnostic
compare that the derived-team path uses later.

instance_cookie is 0 until the agreement vote stamps it in PR1f.

ucc_team_cache_is_cacheable rejects teams that set optional behavioral
params (ORDERING, OUTSTANDING_COLLS, SYNC_TYPE, P2P_CONN, MEM_PARAMS),
since those are not part of the identity and a reuse could silently
change semantics.
Add the team-cache container as a standalone data structure. It owns a
khash uint64 -> ucc_team_t* bucket table plus four intrusive lists (live,
dormant, reserved, pending_destroy), all protected by a single spinlock,
together with the size/capacity bookkeeping and the hit/miss/insert/
eviction counters.

The lifecycle API is refcount based: insert admits a team as DORMANT, get
adopts a DORMANT team into LIVE with refcount++, and put releases a LIVE
team back to DORMANT once the refcount reaches zero. Insert is a no-op at
capacity, which leaves the team uncached but fully functional, and it also
skips a team whose bucket is already occupied by a duplicate identity or a
hash collision. Lookup only ever returns a DORMANT team, so a LIVE or
RESERVED entry is never handed out twice.

The eviction victim picker is FIFO, that is the dormant list head, which is
the oldest insert; LFU selection is added later. The registry helpers do
the list surgery for the state transitions and table_erase removes a team
from the bucket table.

ucc_team_t gains the fields the cache API needs: refcount, cache_identity,
cache_link, cache_state, cache_pending_insert and cache_local_action.

This commit adds no wiring into team create/destroy and no agreement vote;
the lifecycle hooks and the vote machinery are separate changes.
Add the five UCC_TEAM_CACHE_* context config knobs, create and destroy the
per-context cache, and hook the cache into ucc_team_create_post,
ucc_team_create_test and ucc_team_destroy so a destroyed team is retained as
DORMANT and re-adopted by a later create with identical membership.

Each rank classifies its create as a hit or a miss locally, with no cross-rank
agreement. EXACT_REUSE without that agreement is safe as long as team scopes
never overlap, that is, no rank belongs to two simultaneously created teams
with the same membership. The user guide documents this restriction. The
agreement vote that lifts it lands in a follow-up, together with the handling
for the UCC_TEAM_CACHE_AGREE and UCC_TEAM_CACHE_MISS_TEARDOWN states defined
here.

UCC_TEAM_CACHE_ENABLE defaults to n, so the feature is entirely opt-in.
Add gtest integration coverage that drives real create/destroy/recreate cycles
through UccJob: dormant re-adoption, the disabled knob leaving no cache,
eviction with team-id release, id-pool headroom under dormant teams, and the
dump-stats and disable-linear-check knobs.

Add a multi-rank ucc_test_mpi suite, gated on
UCC_TEAM_CACHE_CORRECTNESS_TESTS, covering dormant-reuse hit counts, safety of
a caller ep_map callback freed after the team is cached, and singleton teams.

Document the team cache and its knobs in the user guide, including the
non-overlapping team scope restriction for reuse without agreement.
Members of a cacheable team create classify the create as a cache hit or a
miss from their own cache contents, and those contents can diverge, for
example after an eviction on one rank only. A create where some ranks
re-adopt a dormant team while others build a fresh one does not progress.

Reconcile the per-rank action with a UCC_OP_BAND allreduce over a small
vote buffer before any rank skips the address exchange. The buffer carries
a prepared flag, (value, ~value) equality pairs for the action, key and
instance cookies, and team rank 0's proposed cookie. Any disagreement
degrades the result to MISS, so all members fall back to a fresh build.

This replaces the UCC_TEAM_CACHE_AGREE and UCC_TEAM_CACHE_MISS_TEARDOWN
stubs. A rejected reuse candidate is torn down and rebuilt in place, which
keeps the handle the caller already holds valid.

Add UCC_TEAM_CACHE_AGREEMENT (default y) to control the vote.
Add gtests for the vote helpers: a unanimous EXACT_REUSE agrees and
distributes rank 0's cookie, a single non-preparing rank degrades the
result to MISS, a cookie or parent-cookie mismatch also degrades to MISS,
and next_cookie stays monotonic and never returns 0.

Add two MPI tests. overlap_agreement builds overlapping subcommunicator
sets that, with UCC_TEAM_CACHE_MAX_SIZE=2, evict divergently across ranks;
without the vote this deadlocks, with it the members reconcile to a fresh
build. nonblocking_create_post delays rank 0 and checks that the peers'
ucc_team_create_post returns rather than blocking on the vote.

Document UCC_TEAM_CACHE_AGREEMENT and drop the overlapping-scope
restriction note, which the vote removes.
Add test/mpi/run_cache_equivalence.sh, which runs ucc_test_mpi twice over
the same team set (world, half, odd_even, reverse) and collective set
(barrier, allreduce, bcast, alltoall, allgather) -- once with
UCC_TEAM_CACHE_ENABLE=y and once with n -- using the test's built-in
per-collective correctness checks as the equivalence oracle rather than
diffing outputs across runs.

Both passes also set UCC_TEAM_CACHE_CORRECTNESS_TESTS=y so the cache-on
pass exercises real reuse and derivation. That pass additionally asserts
the correctness suite did not skip itself, so a silently inert cache
cannot masquerade as equivalent by never touching the cache at all.

Wire both the team-cache correctness suite and the equivalence pass into
.ci/scripts/run_tests_ucc_mpi.sh, and distribute the script via
EXTRA_DIST (CI invokes it directly from the source tree).
ucc_service_allreduce_ctx asserted that the context service team was
non-NULL. That team is only created when the context carries an OOB and
UCC_INTERNAL_OOB permits it, and its creation is deliberately non-fatal,
so NULL is a supported runtime state rather than a caller error -
ucc_service_coll_req_init already treats it as one and falls back.

Release builds define NDEBUG, so the assert compiled out and left a NULL
dereference in UCC_TL_TEAM_IFACE. Reproduced with UCC_INTERNAL_OOB=0 and
UCC_TEAM_CACHE_ENABLE=y as a SIGSEGV in ucc_service_allreduce_ctx called
from ucc_team_create_post.

Return UCC_ERR_NOT_SUPPORTED instead. There is deliberately no fallback
to the per-team service team: this collective addresses context ranks
directly, which a team-scoped service team cannot do.

Also realign the assignment block that req->embedded widened, and order
the initialized declaration in ucc_service_coll_finalize first.

(cherry picked from commit 4d564b1400fc46ab1cda4449b1a4eb9ab75e3010)
The cross-rank agreement vote runs over the context service team. That
team is created after the cache is initialized, and its creation is not
fatal, so a context can reach team creation with caching enabled,
agreement requested, and no team to vote over.

Drop the cache in that case rather than continuing without agreement.
Reuse without the vote is safe only when team scopes never overlap, so
disabling agreement automatically would trade a performance feature for
a correctness risk the user never accepted, and the resulting failure is
a hang rather than a slowdown. The condition derives from configuration
alone, so every rank reaches the same decision and the gate stays
uniform.

UCC_TEAM_CACHE_AGREEMENT=n still caches without the vote, which the
warning names; that path is unaffected by this check.

(cherry picked from commit 4ec16e4cb57b5c7f034724b4fb0365daa17cfd8e)
A rank whose team cache failed to allocate was left running without one,
while its peers kept theirs. Such a rank skips the agreement vote in
ucc_team_create_post and its peers wait on a member-scoped allreduce that
never completes. Fail the context instead, so whether a rank caches is
decided by configuration alone, which every rank shares.
ucc_team_destroy_single finalized bp.params.oob whenever the context had a
service team. When ucc_internal_oob_init itself failed, that field still
held the caller's OOB, and the destroy on the create error path freed the
caller's coll_info. Record ownership in team->internal_oob after a
successful init and finalize only what UCC installed.
When the agreement allreduce returned an error the team stayed in
UCC_TEAM_CACHE_AGREE: a RESERVED candidate never returned to the dormant
list, ucc_team_destroy rejected the handle, and a repeated
ucc_team_create_test re-tested the finalized request.

Factor the post-time rollback into a reusable release of the reserved
candidate and use it from the vote path too. A shell this create allocated
moves to a terminal UCC_TEAM_CREATE_FAILED state that ucc_team_destroy
accepts; a handle that named a cached team is handed back and destroy
refuses it, as it does for any dormant or reserved team.

Destroying such a shell also reached ucc_coll_score_free_map with a NULL
map, which the post-time failure path already did; guard it.
A cached team whose teardown failed terminally had its id returned to the
pool while the CL/TL teams that were not destroyed could still match
traffic on it. Leak the id together with the rest of the state, so the
failure stays a bounded leak instead of letting two teams share a tag
domain.
Evicted teams keep their team id until their asynchronous destroy
completes, but the pending-destroy list was only progressed from the next
admission, eviction or context drain. A teardown that stayed in progress
could hold ids inside the pool headroom with nothing driving it.

Register a throttled context progress callback that drives the list when
it is non-empty, and progress it once before a team posts its id-pool
allreduce so finished evictions return their ids first.
The callback-lifetime test freed the poisoned cb_ctx box and immediately
allocated a same-sized replacement with a valid magic. An allocator that
returns the same address would make a retained stale pointer look valid.
Keep the poisoned box allocated until the reuse checks finish.
@bwestheimer
bwestheimer force-pushed the bwestheimer/pub-team-cache-core branch from 628c70f to 754afdc Compare September 28, 2026 17:28

This branch has not been deployed

No deployments
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.

3 participants