Skip to content

UCT/CUDA_COPY: Skip dmabuf probe for async managed memory - #11922

Open
pentschev wants to merge 22 commits into
openucx:masterfrom
pentschev:cuda-async-skip-dmabuf-probe
Open

UCT/CUDA_COPY: Skip dmabuf probe for async managed memory#11922
pentschev wants to merge 22 commits into
openucx:masterfrom
pentschev:cuda-async-skip-dmabuf-probe

Conversation

@pentschev

Copy link
Copy Markdown
Contributor

What?

Fix CUDA async managed mempool memory detection in cuda_copy and add a regression test.

CUDA async managed allocations can report CU_POINTER_ATTRIBUTE_IS_MANAGED with no owning CUDA context. UCX now marks that case as async-managed, so mem_flags detection treats the memory as non-registerable and does not probe dmabuf export for it.

Why?

With UCX_CUDA_COPY_DMABUF=try, posting a UCP receive into CUDA 13 async managed mempool memory can crash inside cuMemGetHandleForAddressRange(). Setting UCX_CUDA_COPY_DMABUF=no avoids the crash because UCX skips the dmabuf export probe.

The crash happens during UCP memory detection when cuda_copy queries MEM_FLAGS: the allocation is managed memory, but because it has no CUDA context, it must be handled like other async managed allocations and kept off the dmabuf/registration path.

How?

The fix is local to cuda_copy memory attribute detection. When cuPointerGetAttributes() reports managed memory and the pointer has no CUDA context, mark it as is_async_managed. The existing mem_flags logic then returns non-registerable flags without calling cuMemGetHandleForAddressRange().

A CUDA 13 regression test allocates async managed memory from the default managed mempool with cudaMallocFromPoolAsync(), queries uct_md_mem_query_v2() with MEM_FLAGS while CUDA_COPY_DMABUF=try, and verifies the memory is classified as cuda-managed and not registrable.

CUDA async managed allocations can report CU_POINTER_ATTRIBUTE_IS_MANAGED with no owning CUDA context. Mark this case as async-managed so mem_flags detection keeps it non-registerable and does not call cuMemGetHandleForAddressRange().

This avoids a CUDA driver crash during UCP memory detection when UCX_CUDA_COPY_DMABUF is enabled. Add a CUDA 13 regression test that queries mem_flags for async managed mempool memory with dmabuf enabled.
@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if (cuda_mem_ctx == NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not gate on md->config.cuda_async_managed here as well, like the sibling branch below? otherwise pool-managed memory is marked non-registrable even when the user sets CUDA_COPY_ASYNC_MEM_TYPE=cuda.

Also worth confirming that ordinary cuMemAllocManaged memory never reports a NULL context (which would otherwise become a real registration regression), and that the config-mismatch above is intentional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, added the same md->config.cuda_async_managed gate so CUDA_COPY_ASYNC_MEM_TYPE=cuda keeps this memory registrable.

cudaMemAllocationTypeManaged));
ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream));

cudaError_t cuda_status = cudaMallocFromPoolAsync(&buffer, size, pool,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe skip instead of asserting here? a device/driver may not support managed mem pools even on CUDART>=13000, and this would turn into a hard test failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the test now skips if the default managed mempool cannot be queried or async managed mempool allocation is unsupported.


cuda_status = cudaStreamSynchronize(stream);
if (cuda_status != cudaSuccess) {
EXPECT_EQ(cudaSuccess, cudaFreeAsync(buffer, stream));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: pls cudaStreamSynchronize before cudaStreamDestroy here, to match the error path above and drain the async free.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if (cuda_mem_ctx == NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: pls add a short comment here explaining why a NULL context indicates an async/stream-ordered (pool) managed allocation; the sibling else-if branch has this reasoning but it's non-obvious in the is_managed path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, added a short comment explaining that managed pool allocations are stream-ordered and have no owning CUDA context.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if (cuda_mem_ctx == NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does a NULL context on genuine managed memory imply it's not registrable? regular cudaMallocManaged/cuMemAllocManaged memory is registrable, and with default PREF_LOC=cpu (sys_dev UNKNOWN) it used to report REGISTRABLE. can we confirm the driver always returns a non-NULL context for ordinary managed allocations, including when allocated under the pushed primary context in uct_cuda_copy_mem_alloc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added positive coverage for ordinary cudaMallocManaged memory and uct_mem_alloc(UCS_MEMORY_TYPE_CUDA_MANAGED). Both assert a non-NULL CUDA context and verify the allocation remains registrable.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Test coverage gap: can we also add a positive test that ordinary cudaMallocManaged memory still reports UCS_MEM_FLAG_REGISTRABLE? The new test only covers the async-managed-pool negative case, so a genuine-managed regression from this heuristic would go uncaught. The existing detect_mem_type_cuda_managed tests only check mem_type, not mem_flags.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Respect CUDA_COPY_ASYNC_MEM_TYPE when marking managed allocations without a CUDA context as async-managed. Add coverage for ordinary cudaMallocManaged memory remaining registrable and make the async managed mempool test skip when the device or driver does not support that allocation path.
@pentschev
pentschev force-pushed the cuda-async-skip-dmabuf-probe branch from e502c49 to 1d53937 Compare September 8, 2026 14:32
@pentschev

Copy link
Copy Markdown
Contributor Author

Test coverage gap: can we also add a positive test that ordinary cudaMallocManaged memory still reports UCS_MEM_FLAG_REGISTRABLE? The new test only covers the async-managed-pool negative case, so a genuine-managed regression from this heuristic would go uncaught. The existing detect_mem_type_cuda_managed tests only check mem_type, not mem_flags.

Done, added managed_mem_registrable to verify ordinary cudaMallocManaged memory still reports UCS_MEM_FLAG_REGISTRABLE.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if ((cuda_mem_ctx == NULL) && md->config.cuda_async_managed) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this gated on md->config.cuda_async_managed? managed pool memory is typed as cuda-managed regardless of ASYNC_MEM_TYPE, so with UCX_CUDA_COPY_ASYNC_MEM_TYPE=cuda we would still report it as registrable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the configuration dependency. The regression test now also runs with CUDA_COPY_ASYNC_MEM_TYPE=cuda, and managed-pool memory remains CUDA_MANAGED and non-registrable.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
if ((cuda_mem_ctx == NULL) && md->config.cuda_async_managed) {
/* Managed pool allocations are stream-ordered and do not have
* an owning CUDA context. */
*is_async_managed = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the default PREF_LOC=cpu this caches sys_dev=UNKNOWN together with mem_flags=0, and ucp_memory_detect_internal() treats that combination as "no info" and falls back to ucp_memory_detect_slowpath() on every operation for these buffers. is the repeated re-detection acceptable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid cache-performance concern. The current cache format cannot distinguish an incomplete UCM allocation event from a completed MD query when both have unknown system device and zero flags. I kept that broader UCS/UCP cache representation change out of this focused bug-fix PR.

void *buffer = nullptr;
cudaError_t cuda_status;

cuda_status = cudaMallocManaged(&buffer, size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use mem_buffer buffer(size, UCS_MEMORY_TYPE_CUDA_MANAGED) here (test/AGENTS.md: reuse mem_buffer for generic allocations); it also releases the buffer if an assertion fails. Currently raw cudaMallocManaged/cudaFree leaks if an assertion aborts the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the raw cudaMallocManaged/cudaFree pair with mem_buffer, so the allocation is released if an assertion aborts the test.

UCS_TEST_SKIP_R("failed to allocate async managed mempool memory");
}

cuda_status = cudaMemsetAsync(buffer, 0, size, stream);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: the three copies of the free/sync/destroy cleanup hide what the test checks; can we wrap the stream and the pool buffer in RAII helpers and keep only the query here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped the stream-ordered managed-pool allocation and stream teardown in cuda_async_managed_mem_buffer. The test body now only performs setup, query, and assertions.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if ((cuda_mem_ctx == NULL) && md->config.cuda_async_managed) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registrability of managed-pool memory is gated on an unrelated config knob. In this branch is_managed is already true, so mem_info->type is UCS_MEMORY_TYPE_CUDA_MANAGED regardless of UCX_CUDA_COPY_ASYNC_MEM_TYPE (md->config.cuda_async_managed only decides the type of non-managed stream-ordered allocations, see the else if at line 758). With UCX_CUDA_COPY_ASYNC_MEM_TYPE=cuda — a configuration already used by existing tests in this file (no_current_context_cuda_registrable) — the same managed-pool buffer is reported as registrable again, i.e. the bug this PR fixes reappears. Registrability is a property of the allocation, not of the reported memory type.

Why gate on md->config.cuda_async_managed here? Suggested alternative:

Suggested change
if ((cuda_mem_ctx == NULL) && md->config.cuda_async_managed) {
if (cuda_mem_ctx == NULL) {
/* Managed pool allocations are stream-ordered and do not have
* an owning CUDA context. */
*is_async_managed = 1;
}

Alternatively, CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE (already queried in cuda_ipc_md.c) detects pool allocations directly, without the ctx heuristic.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
if ((cuda_mem_ctx == NULL) && md->config.cuda_async_managed) {
/* Managed pool allocations are stream-ordered and do not have
* an owning CUDA context. */
*is_async_managed = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached entry becomes indistinguishable from "not detected", forcing a per-operation slow path. When the preferred location resolves to the CPU (PREF_LOC=cpu is the default, and the new positive test uses it), the managed branch sets sys_dev = UCS_SYS_DEVICE_ID_UNKNOWN; with is_async_managed = 1 the cached mem_flags is now 0. ucp_memory_detect_internal() (src/ucp/core/ucp_context.h:746) treats the (sys_dev == UNKNOWN) && (mem_flags == 0) pair as "attributes not detected" and calls ucp_memory_detect_slowpath() on every operation with such a buffer. Before this change these entries were cached with UCS_MEM_FLAG_REGISTRABLE and hit the fast path. Functionally correct, but a hot-path cost for exactly the memory this PR targets — is the per-operation slow path acceptable here?

@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual coverage gap: async_managed_mem_pool_not_registrable is compiled only with CUDART >= 13000 and skips unless the device supports concurrent managed access and managed default mem pools; worth confirming a CI job actually runs it rather than skipping.

Residual coverage gap: Neither new test covers PREF_LOC=gpu, so the managed path where sys_dev is a real device (and mem_flags then depends on dmabuf export) stays untested.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if ((cuda_mem_ctx == NULL) && md->config.cuda_async_managed) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE output might be usable for detection and probably md->config.cuda_async_managed is not exactly related as it should apply to memory that is not is_managed where user can choose memory type to use, so it should be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented this. The code now queries CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE and uses a non-NULL pool handle to identify managed-pool allocations. I also removed the unrelated md->config.cuda_async_managed gate from the managed-memory branch. The managed-pool regression test now covers CUDA_COPY_ASYNC_MEM_TYPE=cuda.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if (cuda_mempool != NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hot-path concern: with the default PREF_LOC=cpu this path caches sys_dev=UNKNOWN together with mem_flags=0, which is exactly the condition that makes ucp_memory_detect_internal() call ucp_memory_detect_slowpath() (uct_md_mem_query_v2 + cuPointerGetAttributes + cuMemRangeGetAttribute + memtype-cache update under a spinlock) on every operation for these buffers. The pre-existing is_async_managed branch avoids this because it gets a real sys_dev; this new branch is the first to produce the UNKNOWN+0 combination. Can we avoid the per-op re-detection, e.g. by reporting the buffer's GPU sys_dev for managed-pool allocations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Managed-pool allocations now use their pointer-reported GPU sys_dev, rather than the CPU preferred-location result. This prevents caching the UNKNOWN plus zero-flags combination and avoids repeated UCP slow-path detection. The regression test now asserts a known system device.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
attr_data[2] = &cuda_device;
attr_type[3] = CU_POINTER_ATTRIBUTE_CONTEXT;
attr_data[3] = &cuda_mem_ctx;
attr_type[4] = CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE and CUmemoryPool are CUDA 11.2+; every other use in the tree is guarded (#if CUDA_VERSION >= 11020 in ucm/cuda/cudamem.c, HAVE_CUDA_FABRIC/HAVE_DECL_SYS_PIDFD_GETFD in cuda_ipc_md.c / cuda_ipc_cache.c). This is the first unguarded use, so building against older CUDA headers would break — do we need a guard here too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added #if CUDA_VERSION >= 11020 guards around the mempool type, fifth pointer attribute, and managed-pool detection path. Older CUDA headers retain the four-attribute query.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;
if (cuda_mempool != NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor (compactness): the if can be a direct assignment:

Suggested change
if (cuda_mempool != NULL) {
/* Managed-pool allocations are stream-ordered and cannot be
* registered. */
*is_async_managed = (cuda_mempool != NULL);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the branch because the managed-pool case now also sets the allocation’s GPU sys_dev, validates it, and skips the preferred-location query. A direct assignment would not express the required locality handling.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
/* Managed-pool allocations are stream-ordered and cannot be
* registered. */
*is_async_managed = 1;
mem_info->sys_dev = uct_cuda_get_sys_dev(cuda_device);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need the cuda_device == CU_DEVICE_CPU handling that the common path below has? for a host-located managed pool uct_cuda_get_sys_dev() returns UNKNOWN and then the whole query fails with UCS_ERR_NO_DEVICE.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The managed-pool path now mirrors the common CPU-device handling: it reports UCS_SYS_DEVICE_ID_UNKNOWN for CU_DEVICE_CPU instead of failing with UCS_ERR_NO_DEVICE.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
attr_type[3] = CU_POINTER_ATTRIBUTE_CONTEXT;
attr_data[3] = &cuda_mem_ctx;
#if CUDA_VERSION >= 11020
attr_type[4] = CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe reuse cuda_mempool for the (cuda_mem_ctx == NULL) heuristic below as well? it identifies stream-ordered allocations directly, so the context-based guess and its driver-bug comment could go away (fine as a follow-up).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as a follow-up. CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE is unavailable before CUDA 11.2, while the existing context heuristic remains the compatibility fallback for older CUDA headers. Removing it here would broaden this bug-fix PR and reduce coverage for that configuration.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual gap (previously reported): CUDART >= 13000 gating plus the concurrent-managed-access / default-managed-pool skips mean the negative tests may never actually execute in CI.

Residual gap (previously reported): PREF_LOC=gpu remains uncovered for the managed path.

Add positive coverage for ordinary managed memory so CUDA managed allocations with an owning context keep reporting UCS_MEM_FLAG_REGISTRABLE. Cover both user cudaMallocManaged memory and memory allocated through uct_mem_alloc.
Use the CUDA mempool pointer attribute to identify managed stream-ordered pool allocations. Such allocations are not registrable regardless of CUDA_COPY_ASYNC_MEM_TYPE, so skip dmabuf probing for them.

Extend the regression test to cover CUDA_COPY_ASYNC_MEM_TYPE=cuda and use RAII for CUDA test allocations.
Report the GPU system device for managed pool allocations so the non-registrable cache entry does not trigger UCP memory detection on every operation. Guard mempool pointer attributes for CUDA versions before 11.2.
Preserve the common CUDA CPU-device handling for managed pool allocations and cover GPU preferred location in the managed pool regression test.
@pentschev
pentschev force-pushed the cuda-async-skip-dmabuf-probe branch from c5fcf7b to 1b27615 Compare September 8, 2026 17:39
@tvegas1
tvegas1 requested a review from rakhmets September 9, 2026 13:10
@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* was not allocated in a context should also allow us to
* identify virtual/stream-ordered CUDA allocations. Keep this
* heuristic for non-managed allocations; managed-pool allocations
* are handled above. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use cuda_mempool != NULL for this check too, and keep cuda_mem_ctx == NULL only as a fallback when the attribute is not available? the mempool handle is exact for stream-ordered allocations, unlike the driver-bug workaround described above.

@@ -124,7 +124,7 @@ class mem_buffer {
/* Allocation mode. */
enum class alloc_mode {
DEFAULT, /* Default allocation mode, using cudaMalloc */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: DEFAULT is also used for cudaMallocManaged now, so the comment is stale.

Suggested change
DEFAULT, /* Default allocation mode, using cudaMalloc */
DEFAULT, /* Default synchronous CUDA allocation mode */

EXPECT_UCS_OK(uct_md_mem_query_v2(md(), buffer.ptr(), size, &mem_attr));
EXPECT_EQ(UCS_MEMORY_TYPE_CUDA_MANAGED, mem_attr.mem_type);
EXPECT_NE(UCS_SYS_DEVICE_ID_UNKNOWN, mem_attr.sys_dev);
EXPECT_FALSE(mem_attr.mem_flags & UCS_MEM_FLAG_REGISTRABLE);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, actually after discussing, should cuda-managed async from mem pool also be marked as registrable without the problematic dmabuf probing removing current mem pool probing changes and simply using something like below as it is real managed memory:

uct_cuda_copy_md_detect_mem_flags(uct_cuda_copy_md_t....)
{

...

if (is_async_managed) {
    return 0;
}
/* Managed memory is never dmabuf-exportable, it is registered via ODP */
if (mem_info->type == UCS_MEMORY_TYPE_CUDA_MANAGED) {
    return UCS_MEM_FLAG_REGISTRABLE;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed mempool-handle probing and its configure check. CUDA-managed memory now returns UCS_MEM_FLAG_REGISTRABLE through ODP before DMABUF export is considered. The regression now expects async managed-pool memory to be registrable while retaining CUDA_COPY_DMABUF=try and PREF_LOC=gpu coverage of the original crash path.

@svc-ucx

svc-ucx commented Sep 9, 2026

Copy link
Copy Markdown

🤖 CI Triage AgentUCX PR (Tests roce on worker 1) · commit 16a911b6

TL;DR: The roce on worker 1 job didn't fail a test — it hung: ucx_perftest stopped producing output for ~18 minutes in the very first UCP stage (ucp_contig_tag_bw/131072, UCX_TLS=rc_verbs,rc_x on mlx5_0:1) and was still alive when the Azure agent was shut down and killed it as an orphan. The reported "agent has received a shutdown signal / operation was canceled" messages are symptoms; the real problem is the hung perftest pair, which needs a re-run plus a backtrace to confirm whether it's environment flakiness or a regression.

Full analysis

Summary: Step Run ./contrib/test_jenkins.sh (perftest phase, run_ucx_perftest 1) hung in the first UCP perftest stage on RoCE device mlx5_0:1; the job ended only because the build agent was stopped ~18 min later.

Root cause: Build, install and installcheck all passed. The last application output is at 14:10:32.811 — the two ucx_perftest ranks (pids 3566241/3566242 on swx-rain03, launched by mpirun -np 2 -x UCX_NET_DEVICES=mlx5_0:1 -x UCX_TLS=rc_verbs,rc_x) print the UCX_TCP_PORT_RANGE unused-env warnings and the +ucp_contig_tag_bw/131072 stage header, then emit nothing at all. The next log line is 14:28:23.668 ##[error]The agent has received a shutdown signal — a 17 min 50 s gap with zero output, which is by far the largest gap in the log and is not legitimate slowness (this stage runs -n 100 messages and normally completes in milliseconds). Confirmation that it was hung, not slow: at cleanup Azure had to Terminate orphan process: pid (3566233) (mpirun), (3566241) (ucx_perftest), (3566242) (ucx_perftest) — the processes were still resident. The script's own guard (TIMEOUT="timeout 200m", contrib/test_jenkins.sh:45) never fired, so the wall-clock kill came from the agent, not from the test framework.

So the hang is in UCP/UCT setup or the first 128 KB tag transfer (rendezvous over rc_verbs/rc_mlx5 on a RoCE port), between the two local ranks. The PR's own changes are confined to src/uct/cuda/cuda_copy/cuda_copy_md.c (10 commits on branch cuda-async-skip-dmabuf-probe, head 16a911b6), and this particular perftest list is built with grep -v cuda and uses host memory over IB transports — so the log does not support a direct causal link. It cannot be fully excluded either, because cuda_copy MD is still opened during ucp_init for every UCP app on this cuda13-HPC-X agent, and the stall happens immediately after config parsing with no UCX output whatsoever, which is consistent with a stall inside uct_cuda_copy_md_open()'s new managed-mempool/dmabuf attribute probing. Confirming this requires a stack trace, which the current log does not provide.

Implicated commit: unknown (no evidence in the log ties the hang to a commit; branch head is [REDACTED:Hex High Entropy String], and recent cuda_copy_md.c commits are 2b732832, [REDACTED:Hex High Entropy String], cfc5a8e4 by Peter Andreas Entschev)

File: contrib/test_jenkins.shrun_ucx_perftest (the mpirun ... ucx_perftest -b test_types_short_ucp -b msg_pow2_short -w 1 invocation); suspect-if-reproducible: src/uct/cuda/cuda_copy/cuda_copy_md.c:92 (uct_cuda_copy_md_is_dmabuf_supported) and the managed-pool detection in uct_cuda_copy_md_open

Suggested fix:

  1. Re-run the roce on worker 1 job. If it hangs again at ucp_contig_tag_bw/131072, treat it as a regression; if it passes, it's an agent/fabric flake plus an agent restart on swx-rain03.
  2. To get actionable evidence, reproduce with UCX_LOG_LEVEL=info (or debug) and grab a backtrace from the stuck ranks (gdb -p <pid> -batch -ex "thread apply all bt"). That immediately distinguishes a stall in uct_cuda_copy_md_open()/CUDA driver init from a stall in RC connection establishment or rendezvous progress.
  3. Independently of the root cause, wrap each ucx_perftest invocation in run_ucx_perftest with a short per-test timeout (e.g. timeout 5m) instead of relying only on the script-wide timeout 200m. As it stands a single hung perftest occupies the agent indefinitely and yields a useless "agent canceled" error rather than a diagnosable failure. Do not raise the job time limit — the process was hung, not making progress.

Related: PR #11922 (#11922) — the PR under test; no existing issue found for this perftest hang signature.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id e1f8a767-8840-4c1e-92c0-d3c2d4ef4e88 in the triage console for the audit trail.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

}

/* Managed memory is registered through ODP and is not dmabuf-exportable. */
if (mem_info->type == UCS_MEMORY_TYPE_CUDA_MANAGED) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this also flip CUDA device symbols to registrable? ucm/cuda/cudamem.c reports symbols (cudaGetSymbolAddress, cuModuleGetGlobal) as UCS_MEMORY_TYPE_CUDA_MANAGED with mem_flags=0, and uct_cuda_copy_md_mem_query() takes the managed type from the memtype cache, so this branch would report them REGISTRABLE and also overwrite the cached mem_flags via ucs_memtype_cache_update().

Rationale: mem_query() does addr_mem_info.type = cached_mem_info.type when the cached type is CUDA_MANAGED (the "device symbols" workaround), then computes flags from addr_mem_info.type and writes them back to the cache. Before this change the symbol range went through the dmabuf probe and stayed non-registrable when export failed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is important. @pentschev #11646 made cuda symbols managed memory as a workaround, and the change we suggested made it so they will now be registerable if queried. The fix needs to make sure it doesn't set registerable to device symbols

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fixed now. mem_query now retains the driver-reported memory type for flag detection before applying the cached CUDA-managed type used to report device symbols. Therefore device symbols continue through the normal non-managed path, remain non-registrable, and their cached zero flags are preserved. I also extended test_cuda_get_symbol_address to verify this after mapping the symbol.

}

UCS_TEST_P(test_mem_alloc_device, user_managed_mem_registrable,
"CUDA_COPY_PREF_LOC=cpu")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_managed_mem_registrable, CUDA_COPY_PREF_LOC=cpu: with PREF_LOC=cpu the managed buffer gets sys_dev=UNKNOWN, so is_registrable() already returned true before this fix and the new branch is not exercised; can we add a PREF_LOC=gpu variant here too? that would also cover the fix on CUDA < 13, where all three async_managed_mem_pool_* tests are compiled out/skipped.


mem_attr.field_mask = UCT_MD_MEM_ATTR_V2_FIELD_MEM_TYPE |
UCT_MD_MEM_ATTR_V2_FIELD_MEM_FLAGS;
EXPECT_UCS_OK(uct_md_mem_query_v2(md(), buffer.ptr(), size, &mem_attr));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: these lines duplicate the tail of query_managed_registrable(); can we split the query + expectations into a small helper and call it from both, keeping only the CU_POINTER_ATTRIBUTE_CONTEXT assert in query_managed_registrable()?

@@ -124,7 +124,7 @@ class mem_buffer {
/* Allocation mode. */
enum class alloc_mode {
DEFAULT, /* Default allocation mode, using cudaMalloc */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: the DEFAULT comment is now stale as well — that path also uses cudaMallocManaged.

Suggested change
DEFAULT, /* Default allocation mode, using cudaMalloc */
DEFAULT, /* Default CUDA allocation mode */
ASYNC /* Asynchronous CUDA allocation mode */

tvegas1
tvegas1 previously approved these changes Sep 10, 2026
* provided address and length as base address and alloc length
* respectively */
mem_info->type = UCS_MEMORY_TYPE_CUDA_MANAGED;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove unrelated change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated
* here, but due to a bug in the driver `cudaMalloc` also returns
* false in that case. Therefore, checking whether the allocation
* was not allocated in a context should also allows us to
* was not allocated in a context should also allow us to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove unrelated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

if (status != UCS_OK) {
printf("memtype cache lookup failed: %s\n", ucs_status_string(status));
++num_errors;
} else if (cached_mem_info.mem_flags & UCS_MEM_FLAG_REGISTRABLE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this hold when dmabuf is not supported? there uct_cuda_copy_md_is_registrable() returns 1 for the symbol (driver type is cuda with a real sys_dev), so the app fails on such a machine. maybe check the flags only when dmabuf is supported?

Comment thread src/uct/cuda/cuda_copy/cuda_copy_md.c Outdated

/* Managed memory is registered through ODP and is not dmabuf-exportable. */
if (mem_info->type == UCS_MEMORY_TYPE_CUDA_MANAGED) {
return UCS_MEM_FLAG_REGISTRABLE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return UCS_MEM_FLAG_REGISTRABLE;
return 1;

minor: the function is a boolean predicate, the other branches return 0/1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

UCT_MD_MEM_ATTR_V2_FIELD_MEM_FLAGS;
EXPECT_UCS_OK(uct_md_mem_query_v2(md(), buffer.ptr(), size, &mem_attr));
EXPECT_EQ(UCS_MEMORY_TYPE_CUDA_MANAGED, mem_attr.mem_type);
EXPECT_TRUE(mem_attr.mem_flags & UCS_MEM_FLAG_REGISTRABLE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: can we reuse query_managed_registrable() here? the only difference is the context check, which can move to its caller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

CUdevice avail_cuda_device = CU_DEVICE_INVALID;
ucs_memory_info_t cached_mem_info;
ucs_memory_info_t addr_mem_info;
ucs_memory_info_t detected_mem_info = {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: initialized variables go above unintialized variables.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@tomerg-nvidia
tomerg-nvidia dismissed their stale review September 10, 2026 14:58

Issue with symbols looks resolved

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

if (status != UCS_OK) {
printf("memtype cache lookup failed: %s\n", ucs_status_string(status));
++num_errors;
} else if (cached_mem_info.mem_flags & UCS_MEM_FLAG_REGISTRABLE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this expects dmabuf export to fail for the symbol, but when the build or device has no dmabuf support (CUDA < 11.7, or UCX_CUDA_COPY_DMABUF=n), uct_cuda_copy_md_is_registrable() returns 1 and this check fails. can we make the expectation independent of dmabuf support?

Rationale: uct_cuda_copy_md_is_dmabuf_supported() compiles the attribute query out below CUDA 11.7, so md->config.dmabuf_supported == 0 and is_registrable() returns 1 at the !md->config.dmabuf_supported short-circuit, i.e. the cached symbol flags would contain UCS_MEM_FLAG_REGISTRABLE. buildlib/pr/cuda/test_malloc_hook.sh runs this app unconditionally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the zero-flags assertion.

EXPECT_EQ(CUDA_SUCCESS, cuMemFree(dptr));
}

UCS_TEST_P(test_mem_alloc_device, user_managed_mem_registrable,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with PREF_LOC=cpu (which is also the default) the managed branch sets sys_dev to unknown, so is_registrable() returns 1 before reaching the new CUDA_MANAGED check - this test and uct_alloc_managed_mem_registrable pass without the fix too. can we use gpu here?

Suggested change
UCS_TEST_P(test_mem_alloc_device, user_managed_mem_registrable,
UCS_TEST_P(test_mem_alloc_device, user_managed_mem_registrable,
"CUDA_COPY_PREF_LOC=gpu")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


void test_async_managed_mem_pool_registrable()
{
constexpr size_t size = 192;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: extra spaces, nothing to align with here.

Suggested change
constexpr size_t size = 192;
constexpr size_t size = 192;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@svc-ucx

svc-ucx commented Sep 10, 2026

Copy link
Copy Markdown

🤖 CI Triage AgentUCX PR (Coverity coverity release on coverity_rh7) · commit 437da172

TL;DR: The Coverity release stage failed on a single new UNINIT defect: src/uct/cuda/cuda_copy/cuda_copy_md.c:1020 copies the whole addr_mem_info struct while its mem_flags field is still uninitialized. Fix by zero-initializing addr_mem_info (= {}) at its declaration (line 992), or by having uct_cuda_copy_md_query_attributes() set mem_flags = 0.

Full analysis

Summary: Azure Pipelines job "Coverity coverity release on coverity_rh7" (build 135586) failed with ##[error]Coverity found 1 issues — a UNINIT (uninitialized scalar variable) defect in the CUDA copy MD; the build and analysis themselves succeeded (551 units compiled, 96 total defect occurrences, gate trips on the 1 newly-formatted error).

Root cause: In uct_cuda_copy_md_mem_query(), ucs_memory_info_t addr_mem_info; is declared without an initializer (line 992). uct_cuda_copy_md_query_attributes() populates only type, sys_dev, base_address and alloc_length — it never writes mem_flags (that field is only assigned later, at line 1074). The new statement detected_mem_info = addr_mem_info; at line 1020 performs a full struct copy, which reads the still-uninitialized mem_flags. Coverity's trace is explicit:

  • step 2 var_decl: "Declaring variable addr_mem_info without initializer" (line 992)
  • step 6 uninit_use: "Using uninitialized value addr_mem_info. Field addr_mem_info.mem_flags is uninitialized." (line 1020)

Note the sibling variable detected_mem_info on line 990 is zero-initialized (= {}), and default_mem_info is zero-filled by its designated initializer — addr_mem_info is the odd one out. This is a real (if benign) uninitialized read, not a false positive.

Implicated commit: a7cbb96f — "UCT/CUDA_COPY: Preserve device symbol flags", Peter Andreas Entschev (introduced the detected_mem_info = addr_mem_info; full-struct copy); tip of branch is 437da172 "UCT/CUDA_COPY: Simplify managed memory checks", same author.

File: src/uct/cuda/cuda_copy/cuda_copy_md.c:1020 (declaration at src/uct/cuda/cuda_copy/cuda_copy_md.c:992)

Suggested fix: Zero-initialize the variable at declaration so the struct copy is well-defined:

-    ucs_memory_info_t addr_mem_info;
+    ucs_memory_info_t addr_mem_info = {};

Alternatives, either of which also silences the checker and is arguably cleaner:

  1. Have uct_cuda_copy_md_query_attributes() explicitly initialize mem_info->mem_flags = 0 on all its return paths (including out_default_range), making the contract "fully populated" instead of "partially populated".
  2. Avoid the full-struct copy at line 1020 and copy only the fields actually needed by uct_cuda_copy_md_detect_mem_flags() (type, sys_dev, base_address, alloc_length) into the already-zeroed detected_mem_info.

Option 1 is the most robust, since addr_mem_info is also fed to ucs_memtype_cache_update() and future field additions to ucs_memory_info_t would hit the same trap.

Related: PR #11922 (branch cuda-async-skip-dmabuf-probe); no pre-existing issue found for this defect — it is newly introduced by this PR's mem_flags/device-symbol work.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

@pentschev

Copy link
Copy Markdown
Contributor Author

🤖 CI Triage AgentUCX PR (Coverity coverity release on coverity_rh7) · commit 437da172

TL;DR: The Coverity release stage failed on a single new UNINIT defect: src/uct/cuda/cuda_copy/cuda_copy_md.c:1020 copies the whole addr_mem_info struct while its mem_flags field is still uninitialized. Fix by zero-initializing addr_mem_info (= {}) at its declaration (line 992), or by having uct_cuda_copy_md_query_attributes() set mem_flags = 0.

Fixed.

int is_host_located = 0;
CUdevice cur_cuda_device = CU_DEVICE_INVALID;
CUdevice avail_cuda_device = CU_DEVICE_INVALID;
ucs_memory_info_t detected_mem_info = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: can we drop the = {}? both structs are fully assigned before any field is read, and the initializers break the = alignment of the block above (docs/CodeStyle.md: consecutive assignments align on one column).

Suggested change
ucs_memory_info_t detected_mem_info = {};
ucs_memory_info_t detected_mem_info;
ucs_memory_info_t addr_mem_info;
ucs_memory_info_t cached_mem_info;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svc-ucx

svc-ucx commented Sep 10, 2026

Copy link
Copy Markdown

🤖 CI Triage AgentUCX PR (Tests roce on worker 0) · commit fae7ff12

TL;DR: The build failed on a single gtest, rcx/test_ucp_fault_tolerance.probe_gated_recovery/6 (rc_x/AM), which is a race-prone test unrelated to this PR's CUDA/dmabuf change — it only samples the transient recovery_arg->probe[lane].comp.func state between short_progress_loop() batches, so a fast recovery makes EXPECT_TRUE(probe_armed) fail. Fix the test to count probe arming deterministically (mock/counter) instead of polling for a window that can open and close inside one progress batch.

Full analysis

Summary: Tests roce on worker 0 (Azure build 135618) failed with make: *** [Makefile:4713: test] Error 1; 8724/8725 tests passed, the only failure being rcx/test_ucp_fault_tolerance.probe_gated_recovery/6, where GetParam() = rc_x/AM.

Root cause: Flaky assertion in the fault-tolerance gtest, not a product regression from this PR. In probe_gated_recovery the test polls with wait_for_cond(...): the condition lambda samples ep->ext->recovery_arg->probe[lane].comp.func != NULL once per iteration, and the progress action is short_progress_loop(), which runs many worker progress iterations per call. On the product side the aux probe window is very short: ucp_ep_recovery_rebuild_p2p_lane() arms the probe (ucp_ep_recovery_arm_probe, src/ucp/core/ucp_ep.c:2172/:2155) and clears it again with ucp_ep_recovery_reset_probe() (src/ucp/core/ucp_ep.c:2188, sets comp.func = NULL) as soon as connect_to_ep_v2 succeeds. Arm → uct_ep_check completion → connect → reset can therefore all happen inside a single short_progress_loop(), and wait_for_cond exits as soon as ucp_ep_get_failed_lanes(ep) == 0, so probe_armed is never observed and EXPECT_TRUE(probe_armed) << "RC p2p lane recovery completed without arming an aux probe" fires. Nothing in the log points at CUDA/dmabuf code (the job is a RoCE worker running an RC AM fault-tolerance test), and no timeout/SIGTERM/hang signature is present — the suite ran to completion in 3321 s with continuous output.

Implicated commit: db208ee "UCP/FT: probe-gated lane recovery via aux uct_ep_check (#11563)", Evgeny Leksikov (introduced both the probe gating and this test). Timing likely aggravated by fb88400 "UCP/WIREUP: Add a token trailer and an ACK message to the lane address exchange (#11843)", Evgeny Leksikov, merged the same day (2026-09-10), which shortens the reply→probe→connect path. Not caused by fae7ff1 (cuda-async-skip-dmabuf-probe).

File: test/gtest/ucp/test_ucp_fault_tolerance.cc:913-943 (assert at :941); product-side probe window src/ucp/core/ucp_ep.c:2072-2097 and src/ucp/core/ucp_ep.c:2188.

Suggested fix: Make the probe observation deterministic instead of sampled:

  • Wrap ep_check with the existing mock_recovery_probe() helper using a counting stub (increment a static counter, then forward/return UCS_INPROGRESS) and assert the counter is non-zero — this cannot be missed by polling; or
  • add a cumulative counter (e.g. worker->counters.recovery_probes incremented in ucp_ep_recovery_arm_probe()) and assert it grew, instead of inspecting probe[lane].comp.func; or
  • as a minimal mitigation, replace short_progress_loop() in that specific wait_for_cond with a single progress() step so the sampling granularity is finer.
    Until fixed, this failure should be treated as unrelated to PR UCT/CUDA_COPY: Skip dmabuf probe for async managed memory #11922 and the job re-run.

Related: #11563 (introduced probe-gated recovery + test), #11843 (LANES_ADDR ACK/token trailer, changes recovery timing), #11397 (precedent: previously disabled a flaky fault-tolerance test)

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.

5 participants