Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update TopologyManager algorithm for selecting "best" non-preferred hint #108154

Merged
merged 3 commits into from
Mar 2, 2022

Conversation

klueska
Copy link
Contributor

@klueska klueska commented Feb 16, 2022

What type of PR is this?

/kind cleanup

What this PR does / why we need it:

For the 'single-numa' and 'restricted' TopologyManager policies, pods are only
admitted if all of their containers have perfect alignment across the set of
resources they are requesting. The best-effort policy, on the other hand,
will prefer allocations that have perfect alignment, but fall back to a
non-preferred alignment if perfect alignment can't be achieved.

The existing algorithm of how to choose the best hint from the set of
"non-preferred" hints is fairly naive and often results in choosing a
sub-optimal hint. It works fine in cases where all resources would end up
coming from a single NUMA node (even if its not the same NUMA nodes), but
breaks down as soon as multiple NUMA nodes are required for the "best"
alignment. We will never be able to achieve perfect alignment with these
non-preferred hints, but we should try and do something more intelligent than
simply choosing the hint with the narrowest mask.

In an ideal world, we would have the TopologyManager return a set of
"resource-relative" hints (as opposed to a common hint for all resources as is
done today). Each resource-relative hint would indicate how many other
resources could be aligned to it on a given NUMA node, and a hint provider
would use this information to allocate its resources in the most aligned way
possible. There are likely some edge cases to consider here, but such an
algorithm would allow us to do partial-perfect-alignment of "some" resources,
even if all resources could not be perfectly aligned.

Unfortunately, supporting something like this would require a major redesign to
how the TopologyManager interacts with its hint providers (as well as how
those hint providers make decisions based on the hints they get back).

That said, we can still do better than the naive algorithm we have today, and
this patch provides a mechanism to do so.

We start by looking at the set of hints passed into the TopologyManager for
each resource and generate a list of the minimum number of NUMA nodes
required to satisfy an allocation for a given resource. In other words, each entry
in this list contains the minNUMAAffinity.Count() for a given resource.

Once we have this list, we find the maximum minNUMAAffinity.Count() from
the list and mark that as the bestNonPreferredAffinityCount that we would like
to have associated with whatever "bestHint" we ultimately generate. The intuition
being that we would like to (at the very least) get alignment for those resources
that require multiple NUMA nodes to satisfy their allocation. If we can't
quite get there, then we should try to come as close to it as possible.

For example, consider a machine where we have 8 NUMA nodes with 32
CPUs per NUMA node and 2 GPUs attached only to each odd numbered
NUMA node (e.g. the DGX-A100 server provided by NVIDIA).

Socket Numa Node CPU(s) GPU(s)
0 0 0-15,128-143  
0 1 16-31,144-159 /dev/nvidia2, /dev/nvidia3
0 2 32-47,160-175  
0 3 48-63,176-191 /dev/nvidia0, /dev/nvidia1
1 4 64-79,192-207  
1 5 80-95,208-223 /dev/nvidia6, /dev/nvidia7
1 6 96-111,224-239  
1 7 112-127,240-255 /dev/nvidia4, /dev/nvidia5

Assuming a machine with no resources allocated yet, if a user were to request 32
CPUs (which can fit on one NUMA node) and 4 GPUs (which requires at least 2
NUMA nodes), then you would expect one of the following affinity masks to win out
as the "best" affinity mask since it encodes the alignment required for all 4 GPUs
to be allocated (even though the 32 CPUs only require a single NUMA node):

Bits:  7 6 5 4 3 2 1 0
      {0 0 0 0 1 0 1 0}
      {0 0 1 0 0 0 1 0}
      {1 0 0 0 0 0 1 0}
      {0 0 1 0 1 0 0 0}
      {1 0 0 0 1 0 0 0}
      {1 0 1 0 0 0 0 0}

However, with the existing algorithm, none of these affinity masks will be
considered, and a naive result of {00000010} will be returned since that is the
"narrowest" alignment that the allocation of all CPUs with some subset of GPUs
can be satisfied with. In effect, the old algorithm lets the "least" constrained
resource influence the hint generation more heavily when what we actually want
is the "more" constrained resource to influence the hint generation more heavily.

To achieve this, the new algorithm proceeds as follows once
we have calculated the bestNonPreferredAffinityCount as described above:

If the mergedHint and bestHint are both non-preferred, then try and find a hint
whose affinity count is as close to (but not higher than) the
bestNonPreferredAffinityCount as possible. To do this we need to consider the
following cases and react accordingly:

  1. bestHint.NUMANodeAffinity.Count() >  bestNonPreferredAffinityCount
  2. bestHint.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount
  3. bestHint.NUMANodeAffinity.Count() <  bestNonPreferredAffinityCount

For case (1), the current bestHint is larger than the
bestNonPreferredAffinityCount, so updating to any narrower mergeHint is
preferred over staying where we are.

For case (2), the current bestHint is equal to the
bestNonPreferredAffinityCount, so we would like to stick with what we have
unless the current mergedHint is also equal to
bestNonPreferredAffinityCount and it is narrower.

For case (3), the current bestHint is less than
bestNonPreferredAffinityCount, so we would like to creep back up to
bestNonPreferredAffinityCount as close as we can. There are three cases to
consider here:

  3a. mergedHint.NUMANodeAffinity.Count() >  bestNonPreferredAffinityCount
  3b. mergedHint.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount
  3c. mergedHint.NUMANodeAffinity.Count() <  bestNonPreferredAffinityCount

For case (3a), we just want to stick with the current bestHint because
choosing a new hint that is greater than bestNonPreferredAffinityCount would
be counter-productive.

For case (3b), we want to immediately update bestHint to the current
mergedHint, making it now equal to bestNonPreferredAffinityCount.

For case (3c), we know that both the current bestHint and the current
mergedHint are less than bestNonPreferredAffinityCount, so we want to
choose one that brings us back up as close to bestNonPreferredAffinityCount
as possible. There are three cases to consider here:

  3ca. mergedHint.NUMANodeAffinity.Count() >  bestHint.NUMANodeAffinity.Count()
  3cb. mergedHint.NUMANodeAffinity.Count() <  bestHint.NUMANodeAffinity.Count()
  3cc. mergedHint.NUMANodeAffinity.Count() == bestHint.NUMANodeAffinity.Count()

For case (3ca), we want to immediately update bestHint to mergedHint
because that will bring us closer to the (higher) value of
bestNonPreferredAffinityCount.

For case (3cb), we want to stick with the current bestHint because choosing
the current mergedHint would strictly move us further away from the
bestNonPreferredAffinityCount.

Finally, for case (3cc), we know that the current bestHint and the current
mergedHint are equal, so we simply choose the narrower of the 2.

This PR implements this algorithm for the case where we must choose from a set
of non-preferred hints and provides a set of unit-tests to verify its
correctness.

Does this PR introduce a user-facing change?

Improved algorithm for selecting "best" non-preferred hint in the TopologyManager

@k8s-ci-robot k8s-ci-robot added kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/needs-sig Indicates an issue or PR lacks a `sig/foo` label and requires one. needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. needs-priority Indicates a PR lacks a `priority/foo` label and requires one. area/kubelet sig/node Categorizes an issue or PR as relevant to SIG Node. and removed do-not-merge/needs-sig Indicates an issue or PR lacks a `sig/foo` label and requires one. labels Feb 16, 2022
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: klueska

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Feb 16, 2022
@klueska
Copy link
Contributor Author

klueska commented Feb 16, 2022

/sig node
/assign @fromanirh @swatisehgal

@swatisehgal
Copy link
Contributor

/triage accepted
/priority important-longterm

@k8s-ci-robot k8s-ci-robot added triage/accepted Indicates an issue or PR is ready to be actively worked on. priority/important-longterm Important over the long term, but may not be staffed and/or may need multiple releases to complete. and removed needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. labels Feb 16, 2022
@k8s-triage-robot
Copy link

Unknown CLA label state. Rechecking for CLA labels.

Send feedback to sig-contributor-experience at kubernetes/community.

/check-cla
/easycla

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Feb 16, 2022
@swatisehgal
Copy link
Contributor

This PR significantly improves the selection process of the best hints in case of non-preferred hints when multiple NUMA nodes are required for the "best" topology alignment. We now have a way more intelligent selection process rather than selecting the narrowest topology hint!

I appreciate the detailed explanation both in the PR description and as comments in the code which helped a lot in the review process.

The PR looks good to me and I am happy to give it an LGTM but while reviewing this, I couldn't stop myself from thinking that if we maintained a list of mergedHints sorted by the corresponding NUMANodeAffinity.Count() we would have made the besthint evaluation logic even more streamlined. Maintaining a sorted list naturally gives us the narrowestHint on top and for the non-preferred hints we essentially have to run a binary search to identify a hint with bestNonPreferredAffinityCount. We might need two lists, one to capture all preferred hints and another one with non-preferred ones. WDYT?

@klueska
Copy link
Contributor Author

klueska commented Feb 17, 2022

The current logic never builds a list of mergedHints at all (such a list could get very large depending on how many different permutations you have to walk through). It also never generates a full list of permutations -- it just iterates through each permutation, calculating the current mergedHint and tracking the current bestHint. I'm worried moving to a model that generates (and stores) full lists for these may cause other unforeseen performance problems.

@swatisehgal
Copy link
Contributor

swatisehgal commented Feb 17, 2022

The current logic never builds a list of mergedHints at all (such a list could get very large depending on how many different permutations you have to walk through). It also never generates a full list of permutations -- it just iterates through each permutation, calculating the current mergedHint and tracking the current bestHint. I'm worried moving to a model that generates (and stores) full lists for these may cause other unforeseen performance problems.

Yeah, I was suggesting that we move to storing the merged hint (evaluated by performing the bitwise AND of a cross product entry) rather than just evaluating by iterating over them.
Hmm, I understand, you do have a valid concern as in order to store mergedhints we need to maintain a sorted list with size equal to product of number of hints from each hintprovider, which (like you said) could get very large and cause performance issues.

I am happy with the PR, going to put on hold so that other reviewers can provide their input.

/lgtm
/hold

@k8s-ci-robot k8s-ci-robot added do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. lgtm "Looks good to me", indicates that a PR is ready to be merged. labels Feb 17, 2022
@klueska
Copy link
Contributor Author

klueska commented Feb 17, 2022

One thing I could do to maybe help write more comprehensive unit tests is to factor out the new logic into a standalone function and then test explicit inputs to that function. I struggled to write comprehensive tests because of the way the bestNonPreferredAffinityCount is calculated across the different resource types.

@k8s-ci-robot k8s-ci-robot removed the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Feb 28, 2022
@pacoxu
Copy link
Member

pacoxu commented Mar 1, 2022

/lgtm
feel free to /unhold

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Mar 1, 2022
For the 'single-numa' and 'restricted' TopologyManager policies, pods are only
admitted if all of their containers have perfect alignment across the set of
resources they are requesting. The best-effort policy, on the other hand, will
prefer allocations that have perfect alignment, but fall back to a non-preferred
alignment if perfect alignment can't be achieved.

The existing algorithm of how to choose the best hint from the set of
"non-preferred" hints is fairly naive and often results in choosing a
sub-optimal hint. It works fine in cases where all resources would end up
coming from a single NUMA node (even if its not the same NUMA nodes), but
breaks down as soon as multiple NUMA nodes are required for the "best"
alignment.  We will never be able to achieve perfect alignment with these
non-preferred hints, but we should try and do something more intelligent than
simply choosing the hint with the narrowest mask.

In an ideal world, we would have the TopologyManager return a set of
"resources-relative" hints (as opposed to a common hint for all resources as is
done today). Each resource-relative hint would indicate how many other
resources could be aligned to it on a given NUMA node, and a  hint provider
would use this information to allocate its resources in the most aligned way
possible. There are likely some edge cases to consider here, but such an
algorithm would allow us to do partial-perfect-alignment of "some" resources,
even if all resources could not be perfectly aligned.

Unfortunately, supporting something like this would require a major redesign to
how the TopologyManager interacts with its hint providers (as well as how those
hint providers make decisions based on the hints they get back).

That said, we can still do better than the naive algorithm we have today, and
this patch provides a mechanism to do so.

We start by looking at the set of hints passed into the TopologyManager for
each resource and generate a list of the minimum number of NUMA nodes required
to satisfy an allocation for a given resource. Each entry in this list then
contains the 'minNUMAAffinity.Count()' for a given resources. Once we have this
list, we find the *maximum* 'minNUMAAffinity.Count()' from the list and mark
that as the 'bestNonPreferredAffinityCount' that we would like to have
associated with whatever "bestHint" we ultimately generate. The intuition being
that we would like to (at the very least) get alignment for those resources
that *require* multiple NUMA nodes to satisfy their allocation. If we can't
quite get there, then we should try to come as close to it as possible.

Once we have this 'bestNonPreferredAffinityCount', the algorithm proceeds as
follows:

If the mergedHint and bestHint are both non-preferred, then try and find a hint
whose affinity count is as close to (but not higher than) the
bestNonPreferredAffinityCount as possible. To do this we need to consider the
following cases and react accordingly:

  1. bestHint.NUMANodeAffinity.Count() >  bestNonPreferredAffinityCount
  2. bestHint.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount
  3. bestHint.NUMANodeAffinity.Count() <  bestNonPreferredAffinityCount

For case (1), the current bestHint is larger than the
bestNonPreferredAffinityCount, so updating to any narrower mergeHint is
preferred over staying where we are.

For case (2), the current bestHint is equal to the
bestNonPreferredAffinityCount, so we would like to stick with what we have
*unless* the current mergedHint is also equal to bestNonPreferredAffinityCount
and it is narrower.

For case (3), the current bestHint is less than bestNonPreferredAffinityCount,
so we would like to creep back up to bestNonPreferredAffinityCount as close as
we can. There are three cases to consider here:

  3a. mergedHint.NUMANodeAffinity.Count() >  bestNonPreferredAffinityCount
  3b. mergedHint.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount
  3c. mergedHint.NUMANodeAffinity.Count() <  bestNonPreferredAffinityCount

For case (3a), we just want to stick with the current bestHint because choosing
a new hint that is greater than bestNonPreferredAffinityCount would be
counter-productive.

For case (3b), we want to immediately update bestHint to the current
mergedHint, making it now equal to bestNonPreferredAffinityCount.

For case (3c), we know that *both* the current bestHint and the current
mergedHint are less than bestNonPreferredAffinityCount, so we want to choose
one that brings us back up as close to bestNonPreferredAffinityCount as
possible. There are three cases to consider here:

  3ca. mergedHint.NUMANodeAffinity.Count() >  bestHint.NUMANodeAffinity.Count()
  3cb. mergedHint.NUMANodeAffinity.Count() <  bestHint.NUMANodeAffinity.Count()
  3cc. mergedHint.NUMANodeAffinity.Count() == bestHint.NUMANodeAffinity.Count()

For case (3ca), we want to immediately update bestHint to mergedHint because
that will bring us closer to the (higher) value of
bestNonPreferredAffinityCount.

For case (3cb), we want to stick with the current bestHint because choosing the
current mergedHint would strictly move us further away from the
bestNonPreferredAffinityCount.

Finally, for case (3cc), we know that the current bestHint and the current
mergedHint are equal, so we simply choose the narrower of the 2.

This patch implements this algorithm for the case where we must choose from a
set of non-preferred hints and provides a set of unit-tests to verify its
correctness.

Signed-off-by: Kevin Klues <kklues@nvidia.com>
@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed lgtm "Looks good to me", indicates that a PR is ready to be merged. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Mar 1, 2022
@klueska
Copy link
Contributor Author

klueska commented Mar 1, 2022

@swatisehgal , @fromanirh , @pacoxu
More extensive unit tests now added.

@ffromani
Copy link
Contributor

ffromani commented Mar 1, 2022

@swatisehgal , @fromanirh , @pacoxu More extensive unit tests now added.

thanks Kevin, will review ASAP
EDIT: ETA March 2 morning.

Copy link
Contributor

@swatisehgal swatisehgal left a comment

Choose a reason for hiding this comment

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

Just noticed a duplicate test but other than that looks good. Please remove that and I will add an LGTM.

pkg/kubelet/cm/topologymanager/policy_test.go Outdated Show resolved Hide resolved
@swatisehgal
Copy link
Contributor

Thanks @klueska for adding comprehensive tests. Your effort is greatly appreciated!
/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Mar 1, 2022
@swatisehgal
Copy link
Contributor

/test pull-kubernetes-e2e-kind-ipv6

pkg/kubelet/cm/topologymanager/policy.go Show resolved Hide resolved
// Finally, for case (3cc), we know that the current bestHint and the
// candidate hint are equal, so we simply choose the narrower of the 2.

// Case 1
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, but still: the above explanation was just great, so I can't help but wonder if would have be even better to intermix it with the actual code (kinda literate-ish programming style) instead of having first pretty long (and very informative) explanation and the chunk of code afterwards.

Copy link
Contributor Author

@klueska klueska Mar 2, 2022

Choose a reason for hiding this comment

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

I actually had it split across them all originally, and I found it a bit harder to follow (even as the author of it). By putting it up front you get the change to read through it all without being interrupted by the code in between. Let me see if I can come up with some middle ground that still flows well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've been wondering myself, and I don't want to slow down this PR needlessly, so up to you!

pkg/kubelet/cm/topologymanager/policy_test.go Show resolved Hide resolved
Copy link
Contributor

@ffromani ffromani left a comment

Choose a reason for hiding this comment

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

This is a great PR and it is worth merging for the great commit message and the code cleanup alone. I've added a bunch of minor comments, but they are suggestions to improve even futher rather than requests, and by no means they require a re-upload.

The only real question I have is the following.
There is the main commit message which goes great lengths in explaining the rationale for the change, and the description is indeed great. If we grok the basic premise, the rest of the code is very clear and follows very smoothly.

The devil's is in the premise itself, I mean specifically here:

We start by looking at the set of hints passed into the TopologyManager for
each resource and generate a list of the minimum number of NUMA nodes required
to satisfy an allocation for a given resource. Each entry in this list then
contains the minNUMAAffinity.Count() for a given resources. Once we have this
list, we find the maximum minNUMAAffinity.Count() from the list and mark
that as the bestNonPreferredAffinityCount'that we would like to have
associated with whatever "bestHint" we ultimately generate. The intuition being
that we would like to (at the very least) get alignment for those resources
that require multiple NUMA nodes to satisfy their allocation. If we can't
quite get there, then we should try to come as close to it as possible.

(emphasis added)
The only problem here is being on the same page about the intuition. From my own past experience, I can think of few examples indeed, but I'm not sure I'm seeing the very same picture you're referring.

An example would be great to make sure we immediately get the same intuition and to make the otherwise flawless explanation really complete.
No need to re-upload, a GH comment is fine here.

@ffromani
Copy link
Contributor

ffromani commented Mar 2, 2022

/lgtm
for the reasons in #108154 (review)
feel free to unhold anytime

@klueska
Copy link
Contributor Author

klueska commented Mar 2, 2022

Thanks for the reviews everyone. Given the only comments are minor nits, I will leave the code as is to avoid requiring one more round of reviews. I will add the example @fromanirh requested to the PR description though. Thanks again.

/unhold

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 2, 2022
@klueska
Copy link
Contributor Author

klueska commented Mar 2, 2022

Description updated.

@ffromani
Copy link
Contributor

ffromani commented Mar 2, 2022

Description updated.

very helpful. Thanks!

@ffromani
Copy link
Contributor

ffromani commented Mar 2, 2022

/test pull-kubernetes-e2e-gce-ubuntu-containerd
all failures seems to be unrelated

@k8s-ci-robot k8s-ci-robot merged commit 422001d into kubernetes:master Mar 2, 2022
SIG Node PR Triage automation moved this from Needs Approver to Done Mar 2, 2022
@k8s-ci-robot k8s-ci-robot added this to the v1.24 milestone Mar 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. area/kubelet cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. lgtm "Looks good to me", indicates that a PR is ready to be merged. needs-priority Indicates a PR lacks a `priority/foo` label and requires one. priority/important-longterm Important over the long term, but may not be staffed and/or may need multiple releases to complete. release-note Denotes a PR that will be considered when it comes time to generate release notes. sig/node Categorizes an issue or PR as relevant to SIG Node. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. triage/accepted Indicates an issue or PR is ready to be actively worked on.
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

None yet

6 participants