On 5/27/22 02:37, Michael Santana wrote:
The handler and CPU mapping in upcalls are incorrect, and this is
specially noticeable systems with cpu isolation enabled.

Say we have a 12 core system where only every even number CPU is enabled
C0, C2, C4, C6, C8, C10

This means we will create an array of size 6 that will be sent to
kernel that is populated with sockets [S0, S1, S2, S3, S4, S5]

The problem is when the kernel does an upcall it checks the socket array
via the index of the CPU, effectively adding additional load on some
CPUs while leaving no work on other CPUs.

e.g.

C0  indexes to S0
C2  indexes to S2 (should be S1)
C4  indexes to S4 (should be S2)

Modulo of 6 (size of socket array) is applied, so we wrap back to S0
C6  indexes to S0 (should be S3)
C8  indexes to S2 (should be S4)
C10 indexes to S4 (should be S5)

Effectively sockets S0, S2, S4 get overloaded while sockets S1, S3, S5
get no work assigned to them

This leads to the kernel to throw the following message:
"openvswitch: cpu_id mismatch with handler threads"

Instead we will send the kernel a corrected array of sockets the size
of all CPUs in the system. In the above example we would create a
corrected array in a round-robin fashion as follows:
[S0, S1, S2, S3, S4, S5, S0, S1, S2, S3, S4, S5]

Fixes: b1e517bd2f81 ("dpif-netlink: Introduce per-cpu upcall dispatch.")

Co-authored-by: Aaron Conole <[email protected]>
signed-off-by: Aaron Conole <[email protected]>
Signed-off-by: Michael Santana <[email protected]>


Hi Ilya,

I sent this patch to get the conversation moving again. It implements the round-robin schema you had proposed.

However, there is a problem with the round robin-schema. But dont worry, I think I have come up with an schema that will make everyone happy

The problem with the round-robin schema (+magic prime number) is that it is active-core agnostic. My understanding from conversations I have had with flavio and aaron is that active cores have a high probability to see traffic (by configuring the NIC to send traffic specifically to those active cores) as this would be the "correctly" configured methodology. We should ideally give priority to these active cores. So we should avoid sharing the same handler thread among cores, specially active cores, as you had also mentioned in this thread.

With the round-robin schema you can easily deliberately create a worse case scenario where 4 active cores are serviced by a single handler thread (s0).

Say you have a 16 core system, with only cores c0, c5, c10, c15 active.
Using the prime number schema we would have 5 handler threads. The mapping would look like this


##             ##             ##                  ##
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15
s0 s1 s2 s3 s4 s0 s1 s2 s3 s4 s0  s1  s2  s3  s4  s0



Instead we can combine the design of the original patch and the round-robin design. Doing so we avoid sharing handler threads among active cores.

First we need more threads than active cores, like in the round-robin (+magic prime number) implementation. I would like to change the prime number to instead something more simple. I would like the total number of handler threads to be: (this obviously only applies when we know we have inactive cores (active_cores < total_cores))

handlers_n = (active_cores)+ min(active_cores/4 +1, inactive_cores/4 +1)

The idea is to assigned each active core a unique handler (active_cores) that will not be shared with any other core. The other handlers min(active_cores/4 +1, inactive_cores/4 +1) will be distributed among the inactive cores in a round-robin fashion. Actually, how we distribute handlers for inactive cores is really up to you. We can optimize for whatever you think would be best. The key take away is that active cores will always get unique handlers that are not shared with any other core. And because we are adding only a small number of additional handlers (mathematically speaking we would always only be adding less than 1/8 of the total cores) for the inactive cores we should not see any additional overhead.


Redoing the above example we would have a total of 6 handlers, 4 are dedicated for the 4 active cores, and 2 shared among the inactive cores in a round-robin fashion

##             ##             ##                  ##
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15
s0 s4 s5 s4 s5 s1 s4 s5 s4 s5 s2  s4  s5  s4  s5  s3

We could alternatively distribute the handlers for inactive cores to

##             ##             ##                  ##
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15
s0 s4 s4 s4 s4 s1 s4 s4 s5 s5 s2  s5  s5  s5  s5  s3



Again, how we distribute the handlers for the inactive cores is up to you. And of course we can play around with the 1/4 +1. If you want more handlers we could do 1/3 +1, or if you want less, 1/5 +1. Whatever you like, we can do :D



But of course if you dont like this implementation we can always fall back to the round-robin implementation in this patch


---
  lib/dpif-netlink.c | 18 ++++++++++++------
  lib/ovs-thread.c   | 16 ++++++++++++++++
  lib/ovs-thread.h   |  1 +
  3 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index 71e35ccdd..77a97beac 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -803,11 +803,11 @@ dpif_netlink_set_handler_pids(struct dpif *dpif_, const 
uint32_t *upcall_pids,
      struct dpif_netlink *dpif = dpif_netlink_cast(dpif_);
      struct dpif_netlink_dp request, reply;
      struct ofpbuf *bufp;
-    int error;
-    int n_cores;
- n_cores = count_cpu_cores();
-    ovs_assert(n_cores == n_upcall_pids);
+    uint32_t *corrected;
+    int error, i, n_cores;
+
+    n_cores = count_total_cores();
      VLOG_DBG("Dispatch mode(per-cpu): Number of CPUs is %d", n_cores);
dpif_netlink_dp_init(&request);
@@ -817,7 +817,12 @@ dpif_netlink_set_handler_pids(struct dpif *dpif_, const 
uint32_t *upcall_pids,
      request.user_features = dpif->user_features |
                              OVS_DP_F_DISPATCH_UPCALL_PER_CPU;
- request.upcall_pids = upcall_pids;
+    corrected = xcalloc(n_cores, sizeof(uint32_t));
+
+    for (i = 0; i < n_cores; i++) {
+        corrected[i] = upcall_pids[i % n_upcall_pids];
+    }
+    request.upcall_pids = corrected;
      request.n_upcall_pids = n_cores;
error = dpif_netlink_dp_transact(&request, &reply, &bufp);
@@ -825,9 +830,10 @@ dpif_netlink_set_handler_pids(struct dpif *dpif_, const 
uint32_t *upcall_pids,
          dpif->user_features = reply.user_features;
          ofpbuf_delete(bufp);
          if (!dpif_netlink_upcall_per_cpu(dpif)) {
-            return -EOPNOTSUPP;
+            error = -EOPNOTSUPP;
          }
      }
+    free(corrected);
      return error;
  }
diff --git a/lib/ovs-thread.c b/lib/ovs-thread.c
index 805cba622..2172b3d3f 100644
--- a/lib/ovs-thread.c
+++ b/lib/ovs-thread.c
@@ -663,6 +663,22 @@ count_cpu_cores(void)
      return n_cores > 0 ? n_cores : 0;
  }
+/* Returns the total number of cores on the system, or 0 if the
+ * number cannot be determined. */
+int
+count_total_cores(void) {
+    long int n_cores;
+
+#ifndef _WIN32
+    n_cores = sysconf(_SC_NPROCESSORS_CONF);
+#else
+    n_cores = 0;
+    errno = ENOTSUP;
+#endif
+
+    return n_cores > 0 ? n_cores : 0;
+}
+
  /* Returns 'true' if current thread is PMD thread. */
  bool
  thread_is_pmd(void)
diff --git a/lib/ovs-thread.h b/lib/ovs-thread.h
index 3b444ccdc..aac5e19c9 100644
--- a/lib/ovs-thread.h
+++ b/lib/ovs-thread.h
@@ -522,6 +522,7 @@ bool may_fork(void);
  /* Useful functions related to threading. */
int count_cpu_cores(void);
+int count_total_cores(void);
  bool thread_is_pmd(void);
#endif /* ovs-thread.h */

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to