Skip to content

UCP/CORE: Add shared topology assignment policy - #11980

Merged
tvegas1 merged 9 commits into
openucx:masterfrom
guy-ealey-morag:shared-assign-policy
Sep 23, 2026
Merged

tvegas1 merged 9 commits into
openucx:masterfrom
guy-ealey-morag:shared-assign-policy

Conversation

@guy-ealey-morag

@guy-ealey-morag guy-ealey-morag commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

What?

  • Add UCX_GPU_NIC_ASSIGNMENT_MODE to control how NICs are assigned to GPUs: auto (default), off, flip, round_robin and shared.
  • Add the shared mode, which assigns all NICs of a topology group to every GPU in that group.
  • auto uses flip on Vera and disables the assignment on other CPUs.
  • Replace the internal assignment policy enum with the mode enum, so that auto and off are handled once, in ucp_context_gpu_nic_assignment_init().
  • Implement all modes in a single ucp_gpu_nic_assignment_add_group().
  • Add tests for the shared mode.

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_context.c Resolved
Comment thread src/ucp/core/ucp_types.h Outdated
* GPU network device assignment mode.
*/
typedef enum {
UCP_GPU_NET_DEVICE_MODE_NOT_SHARED,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

not_shared and not_shared_flip map to the same policy — IMO we can keep only not_shared and use ucp_gpu_nic_assignment_policy_t directly in the config table, dropping this enum and ucp_gpu_net_device_mode_get_policy(). If both are kept, pls document the difference here, like the other mode enums in this file.

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Resolved
config.num_gpus_per_group = num_gpus_per_group;
config.num_nics_per_group = num_nics_per_group;

for (size_t num_gpu_devices = 1; num_gpu_devices <= 2;

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: same shape loop as check_assignment_not_shared; can we keep one loop helper and pass the per-shape check, so the bound comments are not lost?

@svc-ucx

svc-ucx commented Sep 22, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Coverity coverity devel on coverity_rh7) · commit 3f1fa079

TL;DR: The Coverity gate failed with 2 new DIVIDE_BY_ZERO defects in test/gtest/ucp/test_ucp_gpu_nic_assignment.cc (lines 308 and 313), because the new shared_no_nics/shared_no_gpus tests pass 0 for num_nics_per_group/num_gpus_per_group and those values are used as divisors; fix by restructuring the checker to avoid the division (iterate per group) or by adding explicit non-zero guards/asserts after the degenerate-case early return.

Full analysis

Summary: Azure Pipelines job "coverity devel on coverity_rh7" (build 137012) failed at the end of cov-analyze/cov-format-errors with ##[error]Coverity found 2 issues, both DIVIDE_BY_ZERO in the gtest file added/extended by this PR.

Root cause: The build itself succeeded ("745 C/C++ compilation units (100%) successfully", cov-build completed, exit path + nerrors=2 → return 2). Coverity reports:

  • test_ucp_gpu_nic_assignment.cc:426 → check_assignment_shared(2, 3, 0) sets config.num_nics_per_group = 0 (line 339), which flows into check_assignment_shared_inner and is used as a divisor at line 313: nic_idx / config.num_nics_per_group.
  • test_ucp_gpu_nic_assignment.cc:430 → check_assignment_shared(2, 0, 3) sets config.num_gpus_per_group = 0 (line 338), used as a divisor at line 308: gpu_idx / config.num_gpus_per_group.

In practice both divisions are unreachable (with num_gpus_per_group == 0, num_gpus() == 0 triggers the early return at line 297; with num_nics_per_group == 0, num_nics() == 0 so the loop at line 312 never executes), so these are defensive/false-positive findings — but Coverity cannot prove num_gpus() != 0 ⇒ num_gpus_per_group != 0 through the num_groups * num_gpus_per_group product, and the CI gate fails on any non-zero defect count.

Implicated commit: 3f1fa079 — "UCP/CORE: Add shared topology assignment policy", Guy Ealey Morag (the new shared_no_nics / shared_no_gpus tests and check_assignment_shared* helpers). Earlier related: fd1eb5e0 (#11868).

File: test/gtest/ucp/test_ucp_gpu_nic_assignment.cc:308 and :313 (triggered from :426 and :430)

Suggested fix: Remove the divisions so Coverity has nothing to flag — iterate groups explicitly instead of deriving the group index by division, e.g.:

for (size_t group_idx = 0; group_idx < config.num_groups; ++group_idx) {
    for (size_t local_gpu = 0; local_gpu < config.num_gpus_per_group; ++local_gpu) {
        const size_t gpu_idx = (group_idx * config.num_gpus_per_group) + local_gpu;
        ...
        for (size_t nic_group_idx = 0; nic_group_idx < config.num_groups; ++nic_group_idx) {
            for (size_t local_nic = 0; local_nic < config.num_nics_per_group; ++local_nic) {
                const size_t nic_idx = (nic_group_idx * config.num_nics_per_group) + local_nic;
                ...
            }
        }
    }
}

If you prefer a minimal patch, make the non-zero invariants explicit right where the degenerate cases are already handled, so the checker sees them:

if (config.num_gpus() == 0) { ...existing...; return; }
ASSERT_NE(0ul, config.num_gpus_per_group);   /* implied by num_gpus() != 0 */
...
if (config.num_nics() == 0) {
    EXPECT_EQ(0ul, static_cast<size_t>(UCS_STATIC_BITMAP_POPCOUNT(*nic_sys_dev_bitmap)));
    continue;
}
ASSERT_NE(0ul, config.num_nics_per_group); /* before line 313 */

Avoid suppressing via a Coverity annotation unless the restructure is impractical. After the change, re-run the Coverity stage; the published coverity_devel artifact (output/errors/index.html) can be used to confirm the count drops to 0.

Related: PR #11980 (this change); prior GPU/NIC assignment PR #11868.

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_types.h Resolved
Comment thread src/ucp/core/ucp_context.c Resolved
Comment thread src/ucp/core/ucp_context.c Resolved
Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Resolved
const std::vector<size_t> &expected_owners)
void check_assignment_shared_inner(const topology_shape_t &config)
{
ASSERT_NE(config.num_groups, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

these preconditions are copied from check_assignment_not_shared_inner(); maybe move them into the check_assignment() template so both paths share one copy.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual coverage note: the new UCP_GPU_NIC_ASSIGNMENT_POLICY_SHARED path is covered by the new gtest cases, but there is no test or CI job that exercises UCX_GPU_NET_DEVICE_MODE end to end — unavoidable while the config value is not consumed, which is the reason for the comment about the unused config knob.

Comment thread src/ucp/core/ucp_context.c Outdated
Comment on lines +628 to +629
" - not_shared : use the default FLIP assignment.\n"
" - not_shared_flip : use the FLIP assignment.\n"

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.

What's the difference? Why are there two options for this?

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 the original design, I'm looking into changing this to something more intuitive

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.

@tomerg-nvidia Updated

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated


ucp_gpu_nic_assignment_policy_t
ucp_gpu_net_device_mode_get_policy(ucp_gpu_net_device_mode_t mode)

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.

Is there really a need for both ucp_gpu_net_device_mode_t and ucp_gpu_nic_assignment_policy_t?

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.

It is required now after the addition of auto and off

@svc-ucx

svc-ucx commented Sep 22, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Coverity coverity devel on coverity_rh7) · commit 8514fbe7

TL;DR: The Coverity gate failed with 2 new DIVIDE_BY_ZERO defects in test/gtest/ucp/test_ucp_gpu_nic_assignment.cc — the new shared_no_nics/shared_no_gpus tests pass num_nics_per_group=0 / num_gpus_per_group=0 into check_assignment_shared_inner(), which uses those values as divisors at lines 282/288. Add explicit non-zero guards / early-outs for the empty-NIC and empty-GPU cases so the divisions are provably unreachable.

Full analysis

Summary: Azure job "coverity devel on coverity_rh7" (build 137036) failed at the end of cov-analyze: ##[error]Coverity found 2 issues: — both DIVIDE_BY_ZERO in test/gtest/ucp/test_ucp_gpu_nic_assignment.cc:426 and :430. The compile/link and analysis themselves succeeded (745 units, 100%), the gate script returned exit 2 because nerrors=2 > 0.

Root cause: The two new tests added by this PR feed zero-sized topology shapes into the shared-policy checker:

  • shared_no_nics → check_assignment_shared(2, 3, 0) sets config.num_nics_per_group = 0, which is used as a divisor at line 287-288 (nic_idx / config.num_nics_per_group).
  • shared_no_gpus → check_assignment_shared(2, 0, 3) sets config.num_gpus_per_group = 0, used as a divisor at line 282 (gpu_idx / config.num_gpus_per_group).

At runtime both divisions are actually unreachable (num_nics() == num_groups * num_nics_per_group == 0 makes the loop at line 286 not execute, and num_gpus() == 0 hits the early return at line 271-275), so these are conservative/false-positive findings — but Coverity cannot prove that, and the interprocedural path became visible only now because commit [REDACTED:Hex High Entropy String] introduced the check_assignment template + lambda (line 346-349) which lets Coverity propagate the literal 0UL arguments from the test bodies into the divisor. The non-shared path (check_assignment_not_shared_inner) contains no divisions, which is why only the shared variants are flagged.

Implicated commit: [REDACTED:Hex High Entropy String] "TEST/UCP: Add check_assignment template" (Guy Ealey Morag) exposed the path; the divisions and the zero-argument tests come from 3f1fa07 "UCP/CORE: Add shared topology assignment policy" (Guy Ealey Morag).

File: test/gtest/ucp/test_ucp_gpu_nic_assignment.cc:282 and :287-288 (triggered from :426 and :430)

Suggested fix: Make the zero cases explicit in check_assignment_shared_inner() so the divisors are guarded on the same path Coverity analyzes, e.g.:

    check_gpu_device_aliases(config);

    /* No NICs: every GPU must map to an empty bitmap, no division needed */
    if (config.num_nics() == 0) {
        for (size_t gpu_idx = 0; gpu_idx < config.num_gpus(); ++gpu_idx) {
            const auto *bitmap = ucp_gpu_nic_assignment_lookup(
                    &m_assignment, gpu_sys_dev(config, gpu_idx, 0));
            ASSERT_NE(nullptr, bitmap);
            EXPECT_EQ(0, UCS_STATIC_BITMAP_POPCOUNT(*bitmap));
        }
        return;
    }

    /* Reached only when both per-group counts are non-zero */
    ASSERT_NE(0ul, config.num_gpus_per_group);
    ASSERT_NE(0ul, config.num_nics_per_group);

placed right after the existing config.num_gpus() == 0 early return (line 271-275) and before the loop at line 279. The ASSERT_NE pair alone is usually enough for Coverity to drop both findings, and it also documents the precondition. Alternative (less preferred): route the zero-count cases through a dedicated checker instead of check_assignment_shared, or add a Coverity suppression comment — but the guards are cheap and keep the assertion coverage the tests intend.

Related: PR #11980 (branch shared-assign-policy); preceding work in #11868 ("UCP/CORE: Implement GPU to NIC assignment logic").

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 1dd43769-6727-4cac-9ab0-1ac6773b5e67 in the triage console for the audit trail.

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated

switch (mode) {
case UCP_GPU_NIC_ASSIGNMENT_MODE_AUTO:
if (cpu_model != UCS_CPU_MODEL_NVIDIA_VERA) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ucs_topo_build_groups() already refuses to build groups when the CPU model is not Vera, so the check is duplicated here, and flip/round_robin/shared can never take effect on any other hardware even though the help text says they are "required". Can we expose a topo helper for "groups supported" and use it here instead of re-checking the cpu model in UCP?

Comment thread src/ucp/core/ucp_context.c Outdated
"Select how NICs are assigned to GPUs.\n"
" - auto : enable automatically on supported hardware, otherwise disable.\n"
" - off : disable GPU-to-NIC assignment, fallback to free lane selection.\n"
" - flip : require the not shared flip assignment policy.\n"

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: "require the not shared flip assignment policy" is hard to parse; pls describe what each policy does.

Suggested change
" - flip : require the not shared flip assignment policy.\n"
" - flip : assign each NIC to a single GPU, alternating direction.\n"
" - round_robin : assign each NIC to a single GPU in round-robin order.\n"
" - shared : assign all NICs of a group to every GPU in the group.",

const std::vector<size_t> &expected_owners)
void check_assignment_shared_inner(const topology_shape_t &config)
{
ASSERT_NE(config.num_groups, 0);

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 asserts are copied from check_assignment_not_shared_inner(); can we move them into the common check_assignment() wrapper instead?

@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual coverage note: The new UCP_GPU_NIC_ASSIGNMENT_POLICY_SHARED path and the mode resolution are covered by the new gtest cases. There is still no end-to-end coverage of the config knob, and forced modes cannot be exercised on non-Vera CI hardware because ucs_topo_build_groups() rejects other CPU models — related to finding 1 and to the already-reported unused-config-knob comment, which I did not repeat.

Comment thread src/ucp/core/ucp_context.c Outdated

{"GPU_NIC_ASSIGNMENT_MODE", "auto",
"Select how NICs are assigned to GPUs.\n"
" - auto : enable automatically on supported hardware, otherwise disable.\n"

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.

Suggested change
" - auto : enable automatically on supported hardware, otherwise disable.\n"
" - auto : use flip if on supported hardware, otherwise disable.\n"

I think it should mention what policy will be used, no?

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.

In general the policy can be adapted automatically according to whatever hw is detected, so I decided to not be too specific here.

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated

switch (mode) {
case UCP_GPU_NIC_ASSIGNMENT_MODE_AUTO:
if (cpu_model != UCS_CPU_MODEL_NVIDIA_VERA) {

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.

This happens implicitly with the other policies too, no? Is this really needed if the topology groups aren't being created on vera anyway?

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.

When #11976 is merged I would remove the other check and depend only on this function to check if groups and assignments should be built

@svc-ucx

svc-ucx commented Sep 22, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Coverity coverity devel on coverity_rh7) · commit e66bc853

TL;DR: The Coverity devel stage failed the build with 2 new DIVIDE_BY_ZERO defects in the new test_ucp_gpu_nic_assignment.cc gtest — Coverity does not model gtest's ASSERT_NE(...) as a control-flow exit, so the shared_no_nics/shared_no_gpus tests (which deliberately pass 0) look like they reach gpu_idx / config.num_gpus_per_group and nic_idx / config.num_nics_per_group. Replace the ASSERT_NE guards with explicit if (... == 0) { ADD_FAILURE(); return; } early returns so the divisors are provably non-zero.

Full analysis

Summary: Coverity static analysis (cov-analyze on branch devel) reported 2 issues and the pipeline step exited non-zero (##[error]Coverity found 2 issues), both DIVIDE_BY_ZERO in test/gtest/ucp/test_ucp_gpu_nic_assignment.cc at lines 473 and 477.

Root cause: The build/compile and link succeeded ("745 C/C++ compilation units (100%) successfully", "cov-build utility completed successfully"); the failure is purely the analysis gate. Coverity traces:

  • shared_no_nics → check_assignment_shared(2, 3, 0) (line 473) → num_nics_per_group = 0 → divisor in nic_idx / config.num_nics_per_group (line 304).
  • shared_no_gpus → check_assignment_shared(2, 0, 3) (line 477) → num_gpus_per_group = 0 → divisor in gpu_idx / config.num_gpus_per_group (line 299).

At runtime these paths are unreachable: num_gpus() == 0 returns early at line 271/274 and num_nics() == 0 returns early at line 281/291, and lines 279/294 hold ASSERT_NE(..., 0). But the Coverity trace explicitly shows it does not honor the gtest assertion macros as exits — "path: Condition gtest_ar.operator bool(), taking true branch / Falling through to end of if statement" — and it also fails to correlate num_gpus() == 0 (i.e. num_groups * num_gpus_per_group) with num_gpus_per_group == 0. So it is a false positive, newly exposed because the checks are now reached through a template + lambda indirection (check_assignment → operator() → check_assignment_shared_inner), which gives Coverity a concrete interprocedural path with the literal 0 argument.

Implicated commit: [REDACTED:Hex High Entropy String] "TEST/UCP: Add check_assignment template" (Guy Ealey Morag), on top of 3f1fa079 "UCP/CORE: Add shared topology assignment policy"; build HEAD is e66bc853 (same author/PR #11980).

File: test/gtest/ucp/test_ucp_gpu_nic_assignment.cc:279 and :294 (defects reported at :473 and :477; divisions at :299 and :304)

Suggested fix: Make the zero-divisor guards visible to Coverity by turning the gtest assertions into real control flow, e.g.:

    // was: ASSERT_NE(config.num_gpus_per_group, 0);
    if (config.num_gpus_per_group == 0) {
        ADD_FAILURE() << "num_gpus_per_group must be non-zero";
        return;
    }
    ...
    // was: ASSERT_NE(config.num_nics_per_group, 0);
    if (config.num_nics_per_group == 0) {
        ADD_FAILURE() << "num_nics_per_group must be non-zero";
        return;
    }

Do this before the loop at line 296 for num_gpus_per_group and before line 303 for num_nics_per_group. Alternatively, hoist the group indices out of the loops into locals computed only after an explicit non-zero check, or have shared_no_nics/shared_no_gpus call a dedicated check_assignment_shared_empty() helper that never contains a division. Avoid suppressing with a Coverity annotation unless the maintainers prefer that, since an explicit guard is cheap in test code.

Related: PR #11980 (shared-assign-policy); prior work in fd1eb5e0 (#11868, "UCP/CORE: Implement GPU to NIC assignment logic"). No existing issue found for this specific Coverity defect.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 85268753-c30c-425d-affa-f5f81adcf8cb in the triage console for the audit trail.

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_context.c Outdated

{"GPU_NIC_ASSIGNMENT_MODE", "auto",
"Select how NICs are assigned to GPUs.\n"
" - auto : enable automatically on supported hardware, otherwise disable.\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The "not shared" wording is hard to follow for users reading ucx_info -f, and two of the new help lines are 86/87 columns, exceeding the 80-column limit in docs/CodeStyle.md. Suggested rewording:

Suggested change
" - auto : enable automatically on supported hardware, otherwise disable.\n"
" - auto : enable on supported hardware, otherwise disable.\n"
" - off : disable assignment, fall back to free lane selection.\n"
" - flip : assign each NIC to a single GPU, forward then backward.\n"
" - round_robin : assign each NIC to a single GPU, in ascending order.\n"
" - shared : assign all NICs of a group to all GPUs of the group.",


check_gpu_device_aliases(config);

if (config.num_gpus_per_group == 0) {

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 remove this and the num_nics_per_group == 0 check below; num_groups is asserted non-zero and num_gpus() == num_groups * num_gpus_per_group, so the num_gpus() == 0 / num_nics() == 0 early returns above already cover those cases and these ADD_FAILURE() branches are dead code (contradicts the compactness rule in REVIEW.md).

#ifndef UCP_GPU_NIC_ASSIGNMENT_H_
#define UCP_GPU_NIC_ASSIGNMENT_H_

#include "ucp_types.h"

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 define ucp_gpu_nic_assignment_mode_t here next to the closely-coupled policy enum instead of in ucp_types.h, so this self-contained module header does not need to pull in ucp_types.h (and with it ucp/api/ucp.h, uct/api/uct.h)?

Comment thread src/ucp/core/ucp_context.c Resolved
static const char *ucp_gpu_nic_assignment_modes[] = {
[UCP_GPU_NIC_ASSIGNMENT_MODE_AUTO] = "auto",
[UCP_GPU_NIC_ASSIGNMENT_MODE_OFF] = "off",
[UCP_GPU_NIC_ASSIGNMENT_MODE_FLIP] = "flip",

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.

for my understanding: can you pls remind me when/why the flip policy is needed?

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.

It's because only one of the NICs on a CX9 board has SSD memory, so the goal is to have each GPU assigned to one with and one without SSD memory access.

Comment thread src/ucp/core/ucp_types.h Resolved
Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated
} ucp_nics_string_buffers_t;


int ucp_gpu_nic_assignment_policy_resolve(

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.

maybe we drop ucp_gpu_nic_assignment_policy_t and reuse ucp_gpu_nic_assignment_mode_t (or add missing state to existing ucp_gpu_nic_assignment_policy_t)? ucp_rndv_mode_t keeps auto in the same enum, so we would not need a second enum and a mapping between them, it could be:

  • move the policy doc comments onto the mode values in ucp_types.h, and delete the policy enum
  • pass the mode to ucp_gpu_nic_assignment_build(), add_group() and get_gpu_idx()
  • resolve auto to flip/off in ucp_context_gpu_nic_assignment_init(), where the cpu model and the mode names table are already available
  • then ucp_gpu_nic_assignment_policy_resolve() and the mode_resolution test are not needed, and the header does not need ucp_types.h and ucs/arch/cpu.h
  • pls also keep the "not supported on %s architecture" debug from UCS/TOPO: Initialize GPU-NIC assignment object in UCP context #11976 there, and print the resolved mode when it is enabled

the streamlined check in ucp_context_gpu_nic_assignment_init() would be like below and this leaves the PR with just the shared policy, the config knob and the shared tests.

    ucp_gpu_nic_assignment_mode_t mode =
            context->config.ext.gpu_nic_assignment_mode;
    ...

    if (mode == UCP_GPU_NIC_ASSIGNMENT_MODE_AUTO) {
        /* TODO: Improve Vera Rubin detection by checking NICs/GPUs models. */
        mode = (ucs_arch_get_cpu_model() == UCS_CPU_MODEL_NVIDIA_VERA) ?
                       UCP_GPU_NIC_ASSIGNMENT_MODE_FLIP :
                       UCP_GPU_NIC_ASSIGNMENT_MODE_OFF;
    }

    if (mode == UCP_GPU_NIC_ASSIGNMENT_MODE_OFF) {
        ucs_debug("gpu-nic assignment is disabled, cpu model %s",
                  ucs_cpu_model_name());
        return UCS_OK;
    }

    ucs_debug("gpu-nic assignment mode %s, cpu model %s",
              ucp_gpu_nic_assignment_modes[mode], ucs_cpu_model_name());

    /* ... build groups, allocate assignment ... */

    status = ucp_gpu_nic_assignment_build(&groups, mode, assignment);

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated
ucp_gpu_nic_assignment_add_group(ucp_gpu_nic_assignment_t *assignment,
const ucs_topo_group_t *group,
ucp_gpu_nic_assignment_policy_t policy)
ucp_gpu_nic_assignment_add_group_shared(ucp_gpu_nic_assignment_t *assignment,

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.

can we merge add_group_shared() and add_group_not_shared() into a single function either one of the two loop order below so that we have one assignment area?

static void
ucp_gpu_nic_assignment_add_group(ucp_gpu_nic_assignment_t *assignment,
                                 const ucs_topo_group_t *group,
                                 ucp_gpu_nic_assignment_policy_t policy)
{
    size_t num_gpus = ucs_array_length(&group->gpus);
    ucp_gpu_nic_sys_dev_bitmap_t *nic_sys_dev_bitmap;
    const ucs_topo_group_element_t *gpu, *nic;
    size_t gpu_idx, nic_idx;
    ucs_array_for_each_index(gpu, gpu_idx, &group->gpus) {
        ucs_assert((gpu->num_sys_devs > 0) &&
                   (gpu->sys_devs[0] != UCS_SYS_DEVICE_ID_UNKNOWN));
        nic_sys_dev_bitmap = ucs_const_cast(
                ucp_gpu_nic_sys_dev_bitmap_t*,
                ucp_gpu_nic_assignment_lookup(assignment, gpu->sys_devs[0]));
        ucs_assert(nic_sys_dev_bitmap != NULL);
        ucs_array_for_each_index(nic, nic_idx, &group->nics) {
            if ((policy != UCP_GPU_NIC_ASSIGNMENT_POLICY_SHARED) &&
                (ucp_gpu_nic_assignment_get_gpu_idx(policy, num_gpus,
                                                    nic_idx) != gpu_idx)) {
                continue;
            }
            ucp_gpu_nic_bitmap_add_nic(nic_sys_dev_bitmap, nic);
        }
    }
}

iterating the nics first in the shared case sets the same bits, so the tests should not change:

static void
ucp_gpu_nic_assignment_add_group(ucp_gpu_nic_assignment_t *assignment,
                                 const ucs_topo_group_t *group,
                                 ucp_gpu_nic_assignment_mode_t mode)
{
    size_t num_gpus = ucs_array_length(&group->gpus);
    const ucs_topo_group_element_t *gpu, *nic;
    size_t nic_idx, gpu_idx;

    ucs_array_for_each_index(nic, nic_idx, &group->nics) {
        if (mode == UCP_GPU_NIC_ASSIGNMENT_MODE_SHARED) {
            /* Assign the NIC to every GPU in the group. */
            ucs_array_for_each(gpu, &group->gpus) {
                ucp_gpu_nic_assignment_add_nic_to_gpu(assignment, gpu, nic);
            }
        } else {
            gpu_idx = ucp_gpu_nic_assignment_get_gpu_idx(mode, num_gpus,
                                                         nic_idx);
            gpu     = &ucs_array_elem(&group->gpus, gpu_idx);
            ucp_gpu_nic_assignment_add_nic_to_gpu(assignment, gpu, nic);
        }
    }
}

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated
}

static void
ucp_gpu_nic_assignment_add_group(ucp_gpu_nic_assignment_t *assignment,

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 this and inject policy in the called function, implement shared/not shared in one function

UCP_GPU_NIC_ASSIGNMENT_POLICY_LAST;

EXPECT_EQ(params.expected_enabled,
ucp_gpu_nic_assignment_policy_resolve(params.mode,

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.

can we remove that test that checks one enum mapping function, and rely on more behavioral test like below?

}

UCS_TEST_F(test_ucp_gpu_nic_assignment, clique_flip_divisible) {
UCS_TEST_F(test_ucp_gpu_nic_assignment, not_shared_no_gpus) {

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.

pls double check but with a single add_group() as suggested these can go away from the test:

  • shared_no_nics and shared_no_gpus: a zero nic/gpu group now runs the same loop the not_shared tests already cover
  • check_assignment_shared_inner(): it duplicates the preflight asserts, build, empty gpu case and device aliases check
  • the templated check_assignment() wrapper and the not_shared/shared pair, so the existing check_clique_assignment() calls stay as they are

only the shared test itself is needed, for "every nic goes to every gpu of the group".

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated
ucs_assert((gpu->num_sys_devs > 0) &&
(gpu->sys_devs[0] != UCS_SYS_DEVICE_ID_UNKNOWN));

nic_sys_dev_bitmap = ucs_const_cast(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

the gpu bitmap lookup and asserts here are a copy of the block in ucp_gpu_nic_assignment_add_group_not_shared(). can we extract ucp_gpu_nic_assignment_add_nic_to_gpu(assignment, gpu, nic) and call it from both? then shared is just an inner ucs_array_for_each(gpu, &group->gpus) inside the existing NIC loop and the extra dispatch function is not needed. AGENTS.md: "Do not duplicate code or similar implementation patterns; use helper functions".

Comment thread src/ucp/core/ucp_gpu_nic_assignment.h Outdated
*
* @return Nonzero if assignment is enabled, or zero if it is disabled.
*/
int ucp_gpu_nic_assignment_policy_resolve(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

int return plus conditionally-written out param means caller and test must track two values for one decision. maybe return the policy directly and use UCP_GPU_NIC_ASSIGNMENT_POLICY_LAST for "disabled", instead of int + out param? the caller becomes one comparison, and the test no longer needs to assert that policy was left untouched.


check_gpu_device_aliases(config);

if (config.num_gpus_per_group == 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

dead code in the new shared checker: both this guard and the num_nics_per_group == 0 one below are unreachable: num_gpus_per_group == 0 implies num_gpus() == 0, which already returned above (same for nics). pls drop them.

Comment thread src/ucp/core/ucp_context.c Outdated
"Select how NICs are assigned to GPUs.\n"
" - auto : enable automatically on supported hardware, otherwise disable.\n"
" - off : disable GPU-to-NIC assignment, fallback to free lane selection.\n"
" - flip : require the not shared flip assignment policy.\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

help text wording — minor: "require the not shared ... policy" is hard to parse for users. Suggested:

 " - flip        : force the exclusive flip assignment policy.\n"
 " - round_robin : force the exclusive round-robin assignment policy.\n"
 " - shared      : force the shared assignment policy.",

@svc-nvidia-pr-review

Copy link
Copy Markdown

ucs_topo_build_groups_inner() still passes is_vera_rubin=1 unconditionally, so the CX9 NIC filter always runs — the already-reported finding about forced modes on non-Vera hardware.

Residual coverage note: the SHARED policy and mode resolution are covered by the new gtest cases, but there is still no end-to-end coverage of UCX_GPU_NIC_ASSIGNMENT_MODE, since the assignment has no consumer yet and the CX9 filter makes forced modes a no-op on non-Vera CI hardware.

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_gpu_nic_assignment.c Outdated
} ucp_nics_string_buffers_t;


int ucp_gpu_nic_assignment_policy_resolve(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pls add a gtest for ucp_gpu_nic_assignment_policy_resolve() (auto on/off, off, and the forced modes) — the cpu_model argument is injectable exactly for that, and today nothing exercises the mode→policy mapping.

Rationale: the whole user-visible behavior added by this PR is the mode→policy resolution; test_ucp_gpu_nic_assignment.cc still only calls ucp_gpu_nic_assignment_build() with an explicit policy, so auto/off and the enum-to-policy mapping have no coverage at all. This is cheap to test since the function is pure.

Comment thread src/ucp/core/ucp_context.c Outdated
static UCS_CONFIG_DEFINE_ARRAY(memunit_sizes, sizeof(size_t),
UCS_CONFIG_TYPE_MEMUNITS);

/* clang-format off */

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 clang-format off/on markers around the whole config table (and the matching clang-format on at L685) are unrelated to this change, maybe move them to a separate PR.

Rationale: only src/uct/ib/base/ib_device.c uses these markers today, and the two lines wrap ~500 unrelated lines; REVIEW.md asks feature PRs not to carry unrelated changes.

Comment thread src/ucp/core/ucp_types.h
/**
* GPU-to-NIC assignment mode.
*/
typedef enum {

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 per-value comments repeat the config help string in ucp_context.c and the policy doc in ucp_gpu_nic_assignment.h; docs/CodeStyle.md ("Document once, in the most relevant area") — maybe keep only the config help text and leave a pointer here.

Rationale: the flip/round_robin/shared semantics are now written out in three places, so they will drift when a policy is added or changed.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Considered and dropped: ucs_topo_build_groups_inner() still passes is_vera_rubin=1, so the CX9 filter runs for forced modes too, which makes the config help line "The 'flip', 'round_robin' and 'shared' modes apply on any hardware" inaccurate. This is the same concern as the already-reported finding about forced modes on non-Vera hardware, so no new thread was opened (worth a +1 on the existing thread if still unresolved).

Considered and dropped: the unused-config-knob concern (nothing consumes context->gpu_nic_assignment yet) — already reported.

Considered and dropped (preference-level only, no concrete impact): the int + out-param shape of policy_resolve(), the shared-branch loop nesting in ucp_gpu_nic_assignment_add_group(), and the check_clique_assignment → check_assignment test renames (renames may have been requested in an earlier round).

Verified correct (no action needed): the new SHARED path — get_gpu_idx() is bypassed for SHARED (its FLIP/ROUND_ROBIN assert stays valid), groups with no GPUs are still skipped by the caller, each GPU keeps its own bitmap, and the logging/unassigned-NIC paths stay correct. ucs_fatal() is UCS_F_NORETURN, so the default: arm needs no return. context->config.ext is filled by ucp_fill_config() before the new call site, so the mode is valid there.

Residual coverage note: SHARED is covered by the new gtest cases, but ucp_gpu_nic_assignment_policy_resolve() has no test, there is no end-to-end coverage of UCX_GPU_NIC_ASSIGNMENT_MODE (no consumer of the assignment yet), and the unconditional CX9 filter makes forced modes a no-op on non-Vera CI hardware.

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Signed-off-by: Guy Ealey Morag <gealeymorag@nvidia.com>
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

@svc-nvidia-pr-review

Copy link
Copy Markdown

src/ucp/core/ucp_gpu_nic_assignment.c

minor: can we assert on the range instead, so adding a mode does not require updating this list too?

    ucs_assertv((mode >= UCP_GPU_NIC_ASSIGNMENT_MODE_FLIP) &&
                        (mode < UCP_GPU_NIC_ASSIGNMENT_MODE_LAST),
                "invalid gpu-nic assignment mode %d", (int)mode);

Nothing consumes context->gpu_nic_assignment yet, so the new knob has no observable effect.

ucs_topo_build_groups_inner() still passes is_vera_rubin=1 unconditionally, so the CX9 NIC filter runs for forced modes as well; this makes the help line "The 'flip', 'round_robin' and 'shared' modes apply on any hardware" (and the first two help lines about restricting lanes) inaccurate today. Same concern as the existing unresolved thread — worth a +1 there rather than a new thread.

Residual coverage note: the SHARED path and the auto/off resolution are exercised by the new gtest cases, but there is still no end-to-end coverage of UCX_GPU_NIC_ASSIGNMENT_MODE (no consumer of the assignment yet), and the unconditional CX9 filter makes forced modes effectively a no-op on non-Vera CI hardware.

@tvegas1 tvegas1 left a comment

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.

LGTM

Comment thread src/ucp/core/ucp_types.h
UCP_GPU_NIC_ASSIGNMENT_MODE_OFF,

/**
* Assign each NIC, with all of its ports, going forward and then backward

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.

would be good to mention that flip leverage the fact that some hca have immediate access to SSD memory (or better framing)

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.

Deferred to another PR

"for a GPU's memory to the NICs assigned to that GPU.\n"
"All ports of a NIC are assigned together.\n"
"The 'flip', 'round_robin' and 'shared' modes apply on any hardware.\n"
"With 'flip' and 'round_robin', a group with fewer NICs than GPUs leaves\n"

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.

mention somewhere that flip intends to leverage the fact that some hca have immediate access to an SSD hard driver under same pcie switch (or smaller/better framing)

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.

Deferred to another PR

{"GPU_NIC_ASSIGNMENT_MODE", "auto",
"Assign NICs to GPUs within each topology group, and restrict the lanes\n"
"for a GPU's memory to the NICs assigned to that GPU.\n"
"All ports of a NIC are assigned together.\n"

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.

also we can mention that All GPUs with same BDF (MLOParts etc.) share same NIC mapping, or equivalent

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.

Deferred to another PR

"Assign NICs to GPUs within each topology group, and restrict the lanes\n"
"for a GPU's memory to the NICs assigned to that GPU.\n"
"All ports of a NIC are assigned together.\n"
"The 'flip', 'round_robin' and 'shared' modes apply on any hardware.\n"

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.

we now add forced modes on non-vera hardware ending up with no nics at all due to cx9 filter. we need to add like below and we can later always add other filters if needed.

--- a/src/ucs/sys/topo/base/topo_groups.c
+++ b/src/ucs/sys/topo/base/topo_groups.c
@@
 #include <ucs/algorithm/qsort_r.h>
+#include <ucs/arch/cpu.h>
 #include <ucs/datastruct/array.h>
@@ ucs_topo_build_groups_inner(const ucs_topo_sys_device_info_t *devices,
 {
     ucs_topo_group_t inventory;
     ucs_topo_groups_t groups;
     ucs_status_t status;
+    int is_vera_rubin;
 
     ucs_topo_init_groups(&groups);
 
-    status = ucs_topo_groups_inventory_build(devices, num_devices, 1,
-                                             &inventory);
+    /* The cx9 filter only applies to hardware with a known GPU-NIC topology. */
+    is_vera_rubin = (ucs_arch_get_cpu_model() == UCS_CPU_MODEL_NVIDIA_VERA);
+
+    status = ucs_topo_groups_inventory_build(devices, num_devices,
+                                             is_vera_rubin, &inventory);

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.

Deferred to another PR

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.

solved by #11990

@tvegas1 tvegas1 left a comment

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.

need to address last four comments in separate PR

@tvegas1
tvegas1 enabled auto-merge (squash) September 23, 2026 13:09
@tvegas1
tvegas1 merged commit 8ad5130 into openucx:master Sep 23, 2026
162 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants