Linux-NVME Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling
@ 2026-08-21 14:43 Nilay Shroff
  2026-08-21 14:43 ` [PATCH 1/3] shared/net-util-linux: add support for retrieving egress netdev Nilay Shroff
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ messages in thread
From: Nilay Shroff @ 2026-08-21 14:43 UTC (permalink / raw)
  To: linux-nvme
  Cc: dwagner, hare, kbusch, hch, sagi, gjoyce, chaitanyak,
	Nilay Shroff

Hi,

This series is a rework of the earlier patchset[1]. The main
difference is that --nr-io-queues is now calculated in nvme-cli
instead of in the kernel when establishing an NVMe/TCP connection.

This rework is based on the feedback received[2] from the netdev
maintainers.

The original patchset determined the number of NVMe/TCP I/O queues
based on the number of online CPUs and the number of hardware queues
available on the NIC in kernel driver. This series moves that logic
to nvme-cli.

When --nr-io-queues is not explicitly specified, nvme-cli determines
the egress netdev for the NVMe/TCP connection, retrieves its current
hardware queue count, and calculates the default as:

	min(nr_hw_queues, num_online_cpus)

The remaining patches from the original series, which expose NVMe/TCP
queue and flow information through debugfs, are still useful for
subsequent CPU/NIC topology tuning. I will send those changes as a
separate patchset.

The two patchsets are independent and can be reviewed and merged
separately.

As usual, comments, feedback, and suggestions are most welcome!

Thanks!

[1] https://lore.kernel.org/all/20260731073918.614014-1-nilay@linux.ibm.com/
[2] https://lore.kernel.org/all/20260807160904.2eb1b3d1@kernel.org/

Nilay Shroff (3):
  shared/net-util-linux: add support for retrieving egress netdev
  shared/net-util-linux: add support for retrieving NIC h/w queues
  fabrics: determine --nr-io-queues when not explicitly specified

 shared/net-util-linux.c | 265 ++++++++++++++++++++++++++++++++++++++++
 shared/net-util.h       |  32 +++++
 src/fabrics.c           |  77 +++++++++++-
 3 files changed, 371 insertions(+), 3 deletions(-)

-- 
2.53.0



^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/3] shared/net-util-linux: add support for retrieving egress netdev
  2026-08-21 14:43 [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Nilay Shroff
@ 2026-08-21 14:43 ` Nilay Shroff
  2026-08-21 14:43 ` [PATCH 2/3] shared/net-util-linux: add support for retrieving NIC h/w queues Nilay Shroff
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: Nilay Shroff @ 2026-08-21 14:43 UTC (permalink / raw)
  To: linux-nvme
  Cc: dwagner, hare, kbusch, hch, sagi, gjoyce, chaitanyak,
	Nilay Shroff

Add a new shared helper, shr_route_get_egress_iface(), which performs
a route lookup and retrieves the egress netdev for a given destination
IPv4/IPv6 address and, optionally, source IPv4/IPv6 address.

Use netlink to perform the route lookup.

This is a preparatory patch. A subsequent patch will use this helper
to determine the egress netdev when calculating the default
--nr-io-queues value for NVMe/TCP connections.

Signed-off-by: Nilay Shroff <nilay@linux.ibm.com>
---
 shared/net-util-linux.c | 228 ++++++++++++++++++++++++++++++++++++++++
 shared/net-util.h       |  16 +++
 2 files changed, 244 insertions(+)

diff --git a/shared/net-util-linux.c b/shared/net-util-linux.c
index 57c025b05..887755ce9 100644
--- a/shared/net-util-linux.c
+++ b/shared/net-util-linux.c
@@ -7,15 +7,23 @@
  */
 
 #include <arpa/inet.h>
+#include <asm/types.h>
 #include <errno.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
 #include <net/if.h>
 #include <netinet/in.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/socket.h>
+#include <unistd.h>
 
 #include "net-util.h"
 
+#define NETLINK_BUFFER_SIZE 	4096
+#define NETLINK_SEQ_NUM 	1
+
 /*
  * Parse @addr (IPv4, or IPv6 with an optional "%scope" suffix on a
  * link-local address) into @ss. @addr is never a hostname -- resolving one
@@ -181,3 +189,223 @@ bool shr_iface_primary_addr_matches(const struct ifaddrs *iface_list,
 
 	return match_found;
 }
+
+static int rtattr_append(struct nlmsghdr *nlh, char *buf, size_t buflen,
+		unsigned short type, void *attrval, int attrlen)
+{
+	struct rtattr *rta;
+	int nlen = NLMSG_ALIGN(nlh->nlmsg_len);
+	int rlen = RTA_LENGTH(attrlen);
+
+	if (nlen + rlen >= buflen)
+		return -ENOSPC;
+
+	rta = (struct rtattr *)(buf + nlen);
+	rta->rta_type = type;
+	rta->rta_len = rlen;
+	memcpy(RTA_DATA(rta), attrval, attrlen);
+
+	nlh->nlmsg_len = nlen + rlen;
+
+	return 0;
+}
+
+int shr_route_get_egress_iface(const char *saddr, const char *daddr,
+		char *ifname, size_t iflen)
+{
+	struct sockaddr_nl nl = {.nl_family = AF_NETLINK};
+	struct sockaddr_storage ss_src, ss_dst;
+	char buf[NETLINK_BUFFER_SIZE] = {};
+	struct sockaddr_in6 *src6, *dst6;
+	struct sockaddr_in *src, *dst;
+	struct msghdr msg = {};
+	struct nlmsghdr *nlh;
+	struct rtmsg *rtmsg;
+	struct rtattr *rta;
+	int nlen, attrlen;
+	struct iovec iov;
+	int fd, ret = 0;
+
+	if (!daddr)
+		return -EINVAL;
+
+	ret = parse_numeric_addr(daddr, &ss_dst);
+	if (ret < 0) {
+		fprintf(stderr, "Invalid dest address\n");
+		return ret;
+	}
+
+	if (saddr) {
+		ret = parse_numeric_addr(saddr, &ss_src);
+		if (ret < 0) {
+			fprintf(stderr, "Invalid src address\n");
+			return ret;
+		}
+
+		if (ss_src.ss_family != ss_dst.ss_family) {
+			fprintf(stderr, "src/dst address family doesn't match\n");
+			return -EINVAL;
+		}
+	}
+
+	if (iflen < IF_NAMESIZE) {
+		fprintf(stderr, "Invalid ifname length\n");
+		return -EINVAL;
+	}
+
+	fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
+	if (fd < 0) {
+		fprintf(stderr, "Opening netlink socket failed\n");
+		return -errno;
+	}
+
+	/* encode nlmsghdr */
+	nlh = (struct nlmsghdr *)buf;
+
+	nlh->nlmsg_type = RTM_GETROUTE;
+	nlh->nlmsg_flags = NLM_F_REQUEST;
+	nlh->nlmsg_pid = 0; /* to kernel */
+	nlh->nlmsg_seq = NETLINK_SEQ_NUM;
+	nlh->nlmsg_len = sizeof(struct nlmsghdr);
+
+	/* append rtmsg */
+	nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len);
+
+	rtmsg = (struct rtmsg *)(buf + nlh->nlmsg_len);
+	rtmsg->rtm_family = ss_dst.ss_family;
+	rtmsg->rtm_dst_len = (ss_dst.ss_family == AF_INET) ? 32 : 128;
+
+	nlh->nlmsg_len += sizeof(struct rtmsg);
+
+	/* append attribute RTA_DST */
+	if (ss_dst.ss_family == AF_INET) {
+		dst = (struct sockaddr_in *)&ss_dst;
+
+		ret = rtattr_append(nlh, buf, sizeof(buf), RTA_DST,
+				&dst->sin_addr, sizeof(struct in_addr));
+		if (ret)
+			goto out;
+	} else {
+		dst6 = (struct sockaddr_in6 *)&ss_dst;
+
+		ret = rtattr_append(nlh, buf, sizeof(buf), RTA_DST,
+				&dst6->sin6_addr, sizeof(struct in6_addr));
+		if (ret)
+			goto out;
+	}
+
+	if (saddr) {
+		/* append attribute RTA_SRC */
+		if (ss_src.ss_family == AF_INET) {
+			src = (struct sockaddr_in *)&ss_src;
+
+			ret = rtattr_append(nlh, buf, sizeof(buf), RTA_SRC,
+				&src->sin_addr, sizeof(struct in_addr));
+			if (ret)
+				goto out;
+		} else {
+			src6 = (struct sockaddr_in6 *)&ss_src;
+
+			ret = rtattr_append(nlh, buf, sizeof(buf), RTA_SRC,
+				&src6->sin6_addr, sizeof(struct in6_addr));
+			if (ret)
+				goto out;
+		}
+	}
+
+	/* construct sendmsg and send to kernel */
+	iov.iov_base = buf;
+	iov.iov_len = nlh->nlmsg_len;
+
+	msg.msg_iov = &iov;
+	msg.msg_iovlen = 1;
+	msg.msg_name = &nl;
+	msg.msg_namelen = sizeof(nl);
+
+	if (sendmsg(fd, &msg, 0) < 0) {
+		fprintf(stderr, "Sendmsg to kernel failed\n");
+		ret = -errno;
+		goto out;
+	}
+
+	/* construct recvmsg buffer and wait for the response from kernel */
+	memset(&msg, 0, sizeof(msg));
+	memset(buf, 0, sizeof(buf));
+
+	iov.iov_base = buf;
+	iov.iov_len = sizeof(buf);
+
+	msg.msg_iov = &iov;
+	msg.msg_iovlen = 1;
+	msg.msg_name = &nl;
+	msg.msg_namelen = sizeof(nl);
+
+	nlen = recvmsg(fd, &msg, 0);
+	if (nlen < 0) {
+		fprintf(stderr, "Recvmsg from kernel failed\n");
+		ret = -errno;
+		goto out;
+	}
+	close(fd);
+
+	nlh = msg.msg_iov->iov_base;
+
+	for (; NLMSG_OK(nlh, nlen); nlh = NLMSG_NEXT(nlh, nlen)) {
+
+		if (nlh->nlmsg_type == NLMSG_ERROR) {
+			struct nlmsgerr *err = NLMSG_DATA(nlh);
+
+			/*
+			 * Netlink error message is sent as an ack when user
+			 * explicitly requested for it and in which case
+			 * err->error is set to zero.
+			 */
+			if (!err->error)
+				continue;
+
+			fprintf(stderr, "Received nlmsg error: %s\n",
+					strerror(-err->error));
+			break;
+		}
+
+		if (nlh->nlmsg_type == NLMSG_DONE)
+			break;
+
+		/*
+		 * RTM_GETROUTE reply is expected to be of type RTM_NEWROUTE.
+		 */
+		if (nlh->nlmsg_type != RTM_NEWROUTE)
+			continue;
+
+		if (nlh->nlmsg_seq != NETLINK_SEQ_NUM)
+			continue;
+
+		/* parse and decode received netlink message */
+		rtmsg = NLMSG_DATA(nlh);
+		rta = RTM_RTA(rtmsg);
+		attrlen = RTM_PAYLOAD(nlh);
+
+		for (; RTA_OK(rta, attrlen); rta = RTA_NEXT(rta, attrlen)) {
+
+			switch (rta->rta_type) {
+			case RTA_OIF: {
+				int ifindex;
+
+				ifindex = *((int *)RTA_DATA(rta));
+				if (!if_indextoname(ifindex, ifname)) {
+					fprintf(stderr, "if_indextoname failed\n");
+					ret = -errno;
+				}
+				return ret;
+			}
+			default:
+				break;
+			}
+		}
+	}
+
+	return -ENOENT;
+out:
+	close(fd);
+	return ret;
+}
diff --git a/shared/net-util.h b/shared/net-util.h
index 577e582f6..0208923e5 100644
--- a/shared/net-util.h
+++ b/shared/net-util.h
@@ -48,3 +48,19 @@ const char *shr_iface_matching_addr(const struct ifaddrs *iface_list,
  */
 bool shr_iface_primary_addr_matches(const struct ifaddrs *iface_list,
 		const char *iface, const char *addr);
+
+/*
+ * shr_route_get_egress_iface - Lookup route table and find egress inerface
+ *
+ * @saddr: optional source address
+ * @daddr: destintion address
+ * @ifname: buffer to hold the matching iface name
+ * @iflen: length of ifname buffer (must be greater or equal to IF_NAMESIZE)
+ *
+ * Lookup route table using specified saddr and daddr and find the egress
+ * interface. The saddr is optional however daddr is mandatory.
+ *
+ * Return: 0 on success and negative errno on failure.
+ */
+int shr_route_get_egress_iface(const char *saddr, const char *daddr,
+		char *ifname, size_t iflen);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] shared/net-util-linux: add support for retrieving NIC h/w queues
  2026-08-21 14:43 [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Nilay Shroff
  2026-08-21 14:43 ` [PATCH 1/3] shared/net-util-linux: add support for retrieving egress netdev Nilay Shroff
@ 2026-08-21 14:43 ` Nilay Shroff
  2026-08-21 14:43 ` [PATCH 3/3] fabrics: determine --nr-io-queues when not explicitly specified Nilay Shroff
  2026-08-22 21:43 ` [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Sagi Grimberg
  3 siblings, 0 replies; 6+ messages in thread
From: Nilay Shroff @ 2026-08-21 14:43 UTC (permalink / raw)
  To: linux-nvme
  Cc: dwagner, hare, kbusch, hch, sagi, gjoyce, chaitanyak,
	Nilay Shroff

Add a new shared helper, shr_netdev_get_hw_queues(), which retrieves the
NIC h/w queue details using ethtool ioctl ETHTOOL_GCHANNELS.

This is a preparatory patch. A subsequent patch will use this helper
to determine the h/w queue details when calculating the default
--nr-io-queues value for NVMe/TCP connections.

Signed-off-by: Nilay Shroff <nilay@linux.ibm.com>
---
 shared/net-util-linux.c | 37 +++++++++++++++++++++++++++++++++++++
 shared/net-util.h       | 16 ++++++++++++++++
 2 files changed, 53 insertions(+)

diff --git a/shared/net-util-linux.c b/shared/net-util-linux.c
index 887755ce9..7517fccbe 100644
--- a/shared/net-util-linux.c
+++ b/shared/net-util-linux.c
@@ -9,13 +9,16 @@
 #include <arpa/inet.h>
 #include <asm/types.h>
 #include <errno.h>
+#include <linux/ethtool.h>
 #include <linux/netlink.h>
 #include <linux/rtnetlink.h>
+#include <linux/sockios.h>
 #include <net/if.h>
 #include <netinet/in.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/ioctl.h>
 #include <sys/socket.h>
 #include <unistd.h>
 
@@ -409,3 +412,37 @@ out:
 	close(fd);
 	return ret;
 }
+
+int shr_netdev_get_hw_queues(const char *ifname, uint32_t *combined_count,
+		uint32_t *tx_count, uint32_t *rx_count)
+{
+	struct ethtool_channels chan = {};
+	struct ifreq ifr = {};
+	int fd, ret = 0;
+
+	if (!ifname || !combined_count || !tx_count || !rx_count)
+		return -EINVAL;
+
+	fd = socket(AF_INET, SOCK_DGRAM, 0);
+	if (fd < 0) {
+		fprintf(stderr, "opening socket failed\n");
+		return -errno;
+	}
+
+	chan.cmd = ETHTOOL_GCHANNELS;
+	snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
+	ifr.ifr_data = (void *)&chan;
+
+	if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
+		fprintf(stderr, "SIOCETHTOOL failed\n");
+		ret = -errno;
+		goto out;
+	}
+
+	*combined_count = chan.combined_count;
+	*tx_count = chan.tx_count;
+	*rx_count = chan.rx_count;
+out:
+	close(fd);
+	return ret;
+}
diff --git a/shared/net-util.h b/shared/net-util.h
index 0208923e5..59d201210 100644
--- a/shared/net-util.h
+++ b/shared/net-util.h
@@ -8,6 +8,7 @@
 #pragma once
 
 #include <ifaddrs.h>
+#include <inttypes.h>
 #include <stdbool.h>
 
 /*
@@ -64,3 +65,18 @@ bool shr_iface_primary_addr_matches(const struct ifaddrs *iface_list,
  */
 int shr_route_get_egress_iface(const char *saddr, const char *daddr,
 		char *ifname, size_t iflen);
+
+/*
+ * shr_netdev_get_hw_queues - Get h/w queues details for a netdevice
+ *
+ * @ifname: Name of network interface
+ * @combined_count: combined h/w queue count
+ * @tx_count: tx queue count
+ * @rx_count: rx queue count
+ *
+ * Retrieves the NIC h/w queue details.
+ *
+ * Return: 0 on success and negative errno on failure.
+ */
+int shr_netdev_get_hw_queues(const char *ifname, uint32_t *combined_count,
+		uint32_t *tx_count, uint32_t *rx_count);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/3] fabrics: determine --nr-io-queues when not explicitly specified
  2026-08-21 14:43 [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Nilay Shroff
  2026-08-21 14:43 ` [PATCH 1/3] shared/net-util-linux: add support for retrieving egress netdev Nilay Shroff
  2026-08-21 14:43 ` [PATCH 2/3] shared/net-util-linux: add support for retrieving NIC h/w queues Nilay Shroff
@ 2026-08-21 14:43 ` Nilay Shroff
  2026-08-22 21:43 ` [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Sagi Grimberg
  3 siblings, 0 replies; 6+ messages in thread
From: Nilay Shroff @ 2026-08-21 14:43 UTC (permalink / raw)
  To: linux-nvme
  Cc: dwagner, hare, kbusch, hch, sagi, gjoyce, chaitanyak,
	Nilay Shroff

When performing an NVMe/TCP connect operation, if the user does not
explicitly specify --nr-io-queues, determine the default value using:

	min(nr_hw_queues, num_online_cpus)

Here, nr_hw_queues represents the number of hardware queues currently
configured on the NIC used for the NVMe/TCP connection. Use the
shr_route_get_egress_iface() and shr_netdev_get_hw_queues() helpers to
determine the egress netdev and retrieve its hardware queue count.
Use get_nprocs() to determine the number of online CPUs.

Apply the same logic when connecting to the target using a config INI
file.

Signed-off-by: Nilay Shroff <nilay@linux.ibm.com>
---
 src/fabrics.c | 77 +++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 74 insertions(+), 3 deletions(-)

diff --git a/src/fabrics.c b/src/fabrics.c
index ad98c0e38..a640929b6 100644
--- a/src/fabrics.c
+++ b/src/fabrics.c
@@ -19,17 +19,20 @@
  * Fabrics specification standard.
  */
 
+#include <ccan/minmax/minmax.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <getopt.h>
 #include <inttypes.h>
 #include <libgen.h>
+#include <net/if.h>
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/stat.h>
+#include <sys/sysinfo.h>
 #include <sys/types.h>
 #include <time.h>
 #include <unistd.h>
@@ -49,6 +52,7 @@
 #include <ccan/endian/endian.h>
 #include <ccan/str/str.h>
 #include <shared/io-util.h>
+#include <shared/net-util.h>
 #include <shared/sig-util.h>
 
 #include "cleanup.h"
@@ -424,6 +428,48 @@ static int build_conn_tid(const struct libnvmf_config_conn *conn,
 			hostnqn, hostid, tid);
 }
 
+/*
+ * On success returns num of I/O queues and returns 0 on failure or when it's
+ * not possible to determine the I/O queues.
+ */
+static int fabrics_connect_nr_io_queues(const char *transport,
+					const char *traddr,
+					const char *host_traddr,
+					const char *host_iface)
+{
+	uint32_t combined_count, tx_count, rx_count;
+	char ifname[IF_NAMESIZE] = {};
+	int nr_cpus, nr_hw_queues;
+
+	if (strcmp(transport, "tcp"))
+		return 0;
+
+	nr_cpus = get_nprocs();
+	if (nr_cpus <= 0)
+		return 0;
+
+	if (host_iface) {
+		strncpy(ifname, host_iface, IF_NAMESIZE - 1);
+	} else {
+		if (shr_route_get_egress_iface(host_traddr, traddr,
+				ifname, IF_NAMESIZE))
+			return 0;
+	}
+
+	if (shr_netdev_get_hw_queues(ifname, &combined_count,
+			&tx_count, &rx_count))
+		return 0;
+
+	if (combined_count)
+		nr_hw_queues = combined_count;
+	else if (tx_count && rx_count)
+		nr_hw_queues = min(tx_count, rx_count);
+	else
+		return 0;
+
+	return min(nr_cpus, nr_hw_queues);
+}
+
 /* libnvmf_config_conn_for_each() callback: settle addressing/identity,
  * check exclusion, then discover or connect.
  */
@@ -435,6 +481,8 @@ static void consume_conn(const struct libnvmf_config_conn *conn,
 	struct hook_fabrics_data hfd = { .flags = st->flags, .raw = st->raw };
 	__cleanup_nvmf_context struct libnvmf_context *fctx = NULL;
 	__cleanup_nvmf_tid struct libnvmf_tid *tid = NULL;
+	const struct libnvmf_params *params;
+	const char *key = "nr-io-queues";
 	int err;
 
 	if (st->mode == CONSUME_ROLE_BASED && !is_dc && !st->connect)
@@ -460,9 +508,29 @@ static void consume_conn(const struct libnvmf_config_conn *conn,
 		goto record_err;
 
 	err = libnvmf_context_set_connection_from_tid(fctx, tid);
-	if (!err)
-		err = libnvmf_context_apply_params(fctx,
-				libnvmf_config_conn_get_params(conn));
+	if (err)
+		goto record_err;
+
+	params = libnvmf_config_conn_get_params(conn);
+	if (!libnvmf_params_get(params, key)) {
+		int nr_io_queues;
+
+		nr_io_queues = fabrics_connect_nr_io_queues(
+				libnvmf_config_conn_get_transport(conn),
+				libnvmf_config_conn_get_traddr(conn),
+				libnvmf_config_conn_get_host_traddr(conn),
+				libnvmf_config_conn_get_host_iface(conn));
+
+		if (nr_io_queues) {
+			char val[32];
+
+			snprintf(val, sizeof(val), "%d", nr_io_queues);
+			libnvmf_params_set((struct libnvmf_params *)params,
+					key, val);
+		}
+	}
+
+	err = libnvmf_context_apply_params(fctx, params);
 	if (err)
 		goto record_err;
 
@@ -1059,6 +1127,9 @@ int fabrics_connect(const char *desc, int argc, char **argv)
 	if (ret)
 		return ret;
 
+	if (!fa.nr_io_queues)
+		fa.nr_io_queues = fabrics_connect_nr_io_queues(fa.transport,
+				      fa.traddr, fa.host_traddr, fa.host_iface);
 do_connect:
 	ret = nvme_create_global_ctx_hostnqn(&ctx,
 		fa.hostnqn, fa.hostid, &hnqn, &hid);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling
  2026-08-21 14:43 [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Nilay Shroff
                   ` (2 preceding siblings ...)
  2026-08-21 14:43 ` [PATCH 3/3] fabrics: determine --nr-io-queues when not explicitly specified Nilay Shroff
@ 2026-08-22 21:43 ` Sagi Grimberg
  2026-08-24  8:48   ` Nilay Shroff
  3 siblings, 1 reply; 6+ messages in thread
From: Sagi Grimberg @ 2026-08-22 21:43 UTC (permalink / raw)
  To: Nilay Shroff, linux-nvme; +Cc: dwagner, hare, kbusch, hch, gjoyce, chaitanyak



On 21/08/2026 17:43, Nilay Shroff wrote:
> Hi,
>
> This series is a rework of the earlier patchset[1]. The main
> difference is that --nr-io-queues is now calculated in nvme-cli
> instead of in the kernel when establishing an NVMe/TCP connection.
>
> This rework is based on the feedback received[2] from the netdev
> maintainers.
>
> The original patchset determined the number of NVMe/TCP I/O queues
> based on the number of online CPUs and the number of hardware queues
> available on the NIC in kernel driver. This series moves that logic
> to nvme-cli.
>
> When --nr-io-queues is not explicitly specified, nvme-cli determines
> the egress netdev for the NVMe/TCP connection, retrieves its current
> hardware queue count, and calculates the default as:
>
> 	min(nr_hw_queues, num_online_cpus)

This looks reasonable Nilay.

I am wandering tho if we want to place some lower limit here.
For example, my laptop has a virtio device with 4 cpu cores and
a single combined ring:
--
$ lscpu | grep NUMA
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-3
$ ethtool -l enp7s0
Channel parameters for enp7s0:
Pre-set maximums:
RX:        n/a
TX:        n/a
Other:        n/a
Combined:    1
Current hardware settings:
RX:        n/a
TX:        n/a
Other:        n/a
Combined:    1
--

It would be kinda annoying for me to now explicitly pass the nr-io-queues...
I am wandering if some sort of threshold make sense as what you are 
aiming for
is reducing the amount of queues for large cpu counts...


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling
  2026-08-22 21:43 ` [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Sagi Grimberg
@ 2026-08-24  8:48   ` Nilay Shroff
  0 siblings, 0 replies; 6+ messages in thread
From: Nilay Shroff @ 2026-08-24  8:48 UTC (permalink / raw)
  To: Sagi Grimberg, linux-nvme; +Cc: dwagner, hare, kbusch, hch, gjoyce, chaitanyak

On 8/23/26 3:13 AM, Sagi Grimberg wrote:
> 
> 
> On 21/08/2026 17:43, Nilay Shroff wrote:
>> Hi,
>>
>> This series is a rework of the earlier patchset[1]. The main
>> difference is that --nr-io-queues is now calculated in nvme-cli
>> instead of in the kernel when establishing an NVMe/TCP connection.
>>
>> This rework is based on the feedback received[2] from the netdev
>> maintainers.
>>
>> The original patchset determined the number of NVMe/TCP I/O queues
>> based on the number of online CPUs and the number of hardware queues
>> available on the NIC in kernel driver. This series moves that logic
>> to nvme-cli.
>>
>> When --nr-io-queues is not explicitly specified, nvme-cli determines
>> the egress netdev for the NVMe/TCP connection, retrieves its current
>> hardware queue count, and calculates the default as:
>>
>>     min(nr_hw_queues, num_online_cpus)
> 
> This looks reasonable Nilay.
> 
Thank you...

> I am wandering tho if we want to place some lower limit here.
> For example, my laptop has a virtio device with 4 cpu cores and
> a single combined ring:
> -- 
> $ lscpu | grep NUMA
> NUMA node(s):                            1
> NUMA node0 CPU(s):                       0-3
> $ ethtool -l enp7s0
> Channel parameters for enp7s0:
> Pre-set maximums:
> RX:        n/a
> TX:        n/a
> Other:        n/a
> Combined:    1
> Current hardware settings:
> RX:        n/a
> TX:        n/a
> Other:        n/a
> Combined:    1
> -- 
> 
> It would be kinda annoying for me to now explicitly pass the nr-io-queues...
> I am wandering if some sort of threshold make sense as what you are aiming for
> is reducing the amount of queues for large cpu counts...

I think you're running a QEMU guest using user-mode (SLIRP) networking, so having
a combined queue count of 1 is expected.

I also tested this setup before posting the change. With QEMU user-mode networking,
increasing --nr-io-queues beyond 1 (I tried 4 and 8 with vCPU set to match those
numbers) did not improve performance. In fact, limiting --nr-io-queues to 1, which
matches the netdev's single combined queue, gave slightly better performance.

My understanding is that in this topology there is only a single underlying
virtqueue/network queue, so creating multiple NVMe/TCP I/O queues does not provide
additional network parallelism. Instead, those NVMe/TCP queues end up contending
on the same virtqueue/network queue, which can add overhead without providing additional
throughput.

So I'm inclined not to impose an arbitrary lower limit on --nr-io-queues. If the
egress netdev reports only one hardware queue, min(nr_hw_queues, num_online_cpus)
naturally gives 1, which seems like the appropriate choice for this topology. If
the goal is to achieve higher throughput in a QEMU guest, using a networking
configuration with multiqueue support (for example, multiqueue virtio with
vhost-net) would be more appropriate. In that case, the number of available netdev
hardware queues can scale beyond one, and the calculated nr-io-queues can make use
of that parallelism. Alternatively, with PCI/VFIO passthrough, the guest can directly
use the queues exposed by the physical NIC, allowing --nr-io-queues to scale
accordingly.

Thanks,
--Nilay



^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-08-24  8:48 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-21 14:43 [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Nilay Shroff
2026-08-21 14:43 ` [PATCH 1/3] shared/net-util-linux: add support for retrieving egress netdev Nilay Shroff
2026-08-21 14:43 ` [PATCH 2/3] shared/net-util-linux: add support for retrieving NIC h/w queues Nilay Shroff
2026-08-21 14:43 ` [PATCH 3/3] fabrics: determine --nr-io-queues when not explicitly specified Nilay Shroff
2026-08-22 21:43 ` [PATCH 0/3] nvme-cli: NIC topology aware I/O queue scaling Sagi Grimberg
2026-08-24  8:48   ` Nilay Shroff

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox