Hi,
This is a resend of the previous series to include the networking maintainers
and mailing list. There are no code or commit message changes since the
previous posting.
This series has been updated based on the feedback received during LSFMM.
The changelog is updated accordingly.
The NVMe/TCP host driver currently provisions I/O queues primarily based
on CPU availability rather than the capabilities and topology of the
underlying network interface.
On modern systems with many CPUs but fewer NIC hardware queues, this can
lead to multiple NVMe/TCP I/O workers contending for the same TX/RX queue,
resulting in increased lock contention, cacheline bouncing, and degraded
throughput.
This RFC proposes a set of changes to better align NVMe/TCP I/O queues
with NIC queue resources, and to expose queue/flow information to enable
more effective system-level tuning.
Key ideas
---------
1. Scale NVMe/TCP I/O queues based on NIC queue count
Instead of relying solely on CPU count, limit the number of I/O workers
to:
min(num_online_cpus, netdev->real_num_{tx,rx}_queues)
2. Improve CPU locality
Align NVMe/TCP I/O workers with CPUs associated with NIC IRQ affinity
to reduce cross-CPU traffic and improve cache locality.
3. Expose queue and flow information via debugfs
Export per-I/O queue information including:
- queue id (qid)
- CPU affinity
- TCP flow (src/dst IP and ports)
This enables userspace tools to configure:
- IRQ affinity
- RPS/XPS
- ntuple steering
- or any other scaling as deemed feasible
4. Provide infrastructure for extensible debugfs support in NVMe
Together, these changes allow better alignment of:
flow -> NIC queue -> IRQ -> CPU -> NVMe/TCP I/O worker
Performance Evaluation
----------------------
Tests were conducted using fio over NVMe/TCP with the following parameters:
ioengine=io_uring
direct=1
bs=4k
numjobs=<#nic-queues>
iodepth=64
System:
CPUs: 72
NIC: 100G mlx5
Two configurations were evaluated.
Scenario 1: NIC queues < CPU count
----------------------------------
- CPUs: 72
- NIC queues: 32
Baseline Patched Patched + tuning
randread 3141 MB/s 3228 MB/s 7509 MB/s
(767k IOPS) (788k IOPS) (1833k IOPS)
randwrite 4510 MB/s 6172 MB/s 7518 MB/s
(1101k IOPS) (1507k IOPS) (1836k IOPS)
randrw (read) 2156 MB/s 2560 MB/s 3932 MB/s
(526k IOPS) (625k IOPS) (960k IOPS)
randrw (write) 2155 MB/s 2560 MB/s 3932 MB/s
(526k IOPS) (625k IOPS) (960k IOPS)
Observation:
When CPU count exceeds NIC queue count, the baseline configuration
suffers from queue contention. The proposed changes provide modest
improvements on their own, and when combined with queue-aware tuning
(IRQ affinity, ntuple steering, and CPU alignment), enable up to
~1.5x–2.5x throughput improvement.
Scenario 2: NIC queues == CPU count
-----------------------------------
- CPUs: 72
- NIC queues: 72
Baseline Patched + tuning
randread 4310 MB/s 7987 MB/s
(1052k IOPS) (1950k IOPS)
randwrite 7947 MB/s 7972 MB/s
(1940k IOPS) (1946k IOPS)
randrw (read) 3583 MB/s 4030 MB/s
(875k IOPS) (984k IOPS)
randrw (write) 3583 MB/s 4029 MB/s
(875k IOPS) (984k IOPS)
Observation:
When NIC queues are already aligned with CPU count, the baseline performs
well. The proposed changes maintain write performance (no regression) and
still improve read and mixed workloads due to better flow-to-CPU locality.
Notes on tuning
---------------
The "patched + tuning" configuration includes:
- aligning NVMe/TCP I/O workers with NIC queue count
- IRQ affinity configuration per RX queue
- ntuple-based flow steering
- CPU/queue affinity alignment
These tuning steps are enabled by the queue/flow information exposed through
this patchset.
As usual, feedback/comment/suggestions are most welcome!
Changes from v1:
- remove the "match-hw-queues" fabric option; always limit the number
of NVMe/TCP I/O queues to min(num_online_cpus, num_nic_queues)
- drop the diagnostic patch reporting NIC queue underutilization
- split the netdev helper into a separate patch
- move the netdev helper implementation to net/core/dev.c
Link to v1: https://lore.kernel.org/all/20260420115716.3071293-1-nilay@linux.ibm.com/
Nilay Shroff (4):
net: add helper for device lookup by destination address
nvme-tcp: limit I/O queue count based on NIC queue count
nvme: add debugfs helpers for NVMe drivers
nvme: expose queue information via debugfs
drivers/nvme/host/Makefile | 2 +-
drivers/nvme/host/core.c | 3 +
drivers/nvme/host/debugfs.c | 191 ++++++++++++++++++++++++++++++++++++
drivers/nvme/host/nvme.h | 12 +++
drivers/nvme/host/tcp.c | 115 ++++++++++++++++++++++
include/linux/netdevice.h | 5 +
net/core/dev.c | 84 ++++++++++++++++
7 files changed, 411 insertions(+), 1 deletion(-)
create mode 100644 drivers/nvme/host/debugfs.c
--
2.53.0
Add netdev_get_by_addr(), a helper that looks up the routing table to
retrieve the network device associated with a destination address. The
helper also supports netdev reference tracking.
Signed-off-by: Nilay Shroff <redacted>
---
include/linux/netdevice.h | 5 +++
net/core/dev.c | 84 +++++++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+)
NVMe-TCP currently provisions I/O queues based primarily on the number
of online CPUs. On systems where the CPU count significantly exceeds the
number of NIC hardware queues, multiple NVMe-TCP I/O queues end up
sharing the same NIC TX/RX queues. This increases lock contention,
cacheline bouncing, and inter-processor interrupts (IPIs), reducing I/O
efficiency.
Limit the number of NVMe-TCP default I/O queues to the smaller of the
number of online CPUs and the number of NIC hardware queues. Aligning
the number of NVMe-TCP I/O queues with the NIC queue topology reduces
queue sharing, improves locality, and can improve throughput while
reducing tail latency.
The number of NVMe-TCP I/O queues is now limited to:
min(num_online_cpus, num_nic_queues)
Signed-off-by: Nilay Shroff <redacted>
---
drivers/nvme/host/tcp.c | 61 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
Introduce helper APIs that allow NVMe drivers to register and unregister
debugfs entries, along with a reusable attribute structure for defining
new debugfs files.
The implementation uses seq_file interfaces to safely expose per-
namespace or per-path statistics, while supporting both simple show
callbacks and full seq_operations.
This will be used by subsequent patches to expose NVMe-TCP queue
and flow information for tuning NVMe TCP I/O workqueue and network stack
components.
Signed-off-by: Nilay Shroff <redacted>
---
drivers/nvme/host/Makefile | 2 +-
drivers/nvme/host/debugfs.c | 129 ++++++++++++++++++++++++++++++++++++
drivers/nvme/host/nvme.h | 10 +++
3 files changed, 140 insertions(+), 1 deletion(-)
create mode 100644 drivers/nvme/host/debugfs.c
Add a new debugfs attribute "io_queue_info" to expose per-queue
information for NVMe controllers. For NVMe-TCP, this includes the
CPU handling each I/O queue and the associated TCP flow (source and
destination address/port).
This information can be useful for understanding and tuning the
interaction between NVMe-TCP I/O queues and network stack components,
such as IRQ affinity, RPS/RFS, XPS, or NIC flow steering (ntuple).
The data is exported using seq_file interfaces to allow iteration
over all controller queues.
Signed-off-by: Nilay Shroff <redacted>
---
drivers/nvme/host/core.c | 3 ++
drivers/nvme/host/debugfs.c | 64 ++++++++++++++++++++++++++++++++++++-
drivers/nvme/host/nvme.h | 2 ++
drivers/nvme/host/tcp.c | 54 +++++++++++++++++++++++++++++++
4 files changed, 122 insertions(+), 1 deletion(-)
Just a nit, I don't think that raising a warning just because
kzalloc() returned NULL is a good idea as failures can naturally
occur under OOM conditions and aren't bugs.
Maurizio
From: Stanislav Fomichev <hidden> Date: 2026-07-31 16:41:12
On 07/31, Nilay Shroff wrote:
Hi,
This is a resend of the previous series to include the networking maintainers
and mailing list. There are no code or commit message changes since the
previous posting.
This series has been updated based on the feedback received during LSFMM.
The changelog is updated accordingly.
The NVMe/TCP host driver currently provisions I/O queues primarily based
on CPU availability rather than the capabilities and topology of the
underlying network interface.
On modern systems with many CPUs but fewer NIC hardware queues, this can
lead to multiple NVMe/TCP I/O workers contending for the same TX/RX queue,
resulting in increased lock contention, cacheline bouncing, and degraded
throughput.
This RFC proposes a set of changes to better align NVMe/TCP I/O queues
with NIC queue resources, and to expose queue/flow information to enable
more effective system-level tuning.
Key ideas
---------
1. Scale NVMe/TCP I/O queues based on NIC queue count
Instead of relying solely on CPU count, limit the number of I/O workers
to:
min(num_online_cpus, netdev->real_num_{tx,rx}_queues)
2. Improve CPU locality
Align NVMe/TCP I/O workers with CPUs associated with NIC IRQ affinity
to reduce cross-CPU traffic and improve cache locality.
3. Expose queue and flow information via debugfs
Export per-I/O queue information including:
- queue id (qid)
- CPU affinity
- TCP flow (src/dst IP and ports)
[..]
This enables userspace tools to configure:
- IRQ affinity
- RPS/XPS
- ntuple steering
- or any other scaling as deemed feasible
Can you expand on this a bit? What specifically helped the most for your
tuned case?
From: Stanislav Fomichev <hidden> Date: 2026-07-31 16:41:14
On 07/31, Nilay Shroff wrote:
quoted hunk
NVMe-TCP currently provisions I/O queues based primarily on the number
of online CPUs. On systems where the CPU count significantly exceeds the
number of NIC hardware queues, multiple NVMe-TCP I/O queues end up
sharing the same NIC TX/RX queues. This increases lock contention,
cacheline bouncing, and inter-processor interrupts (IPIs), reducing I/O
efficiency.
Limit the number of NVMe-TCP default I/O queues to the smaller of the
number of online CPUs and the number of NIC hardware queues. Aligning
the number of NVMe-TCP I/O queues with the NIC queue topology reduces
queue sharing, improves locality, and can improve throughput while
reducing tail latency.
The number of NVMe-TCP I/O queues is now limited to:
min(num_online_cpus, num_nic_queues)
Signed-off-by: Nilay Shroff <redacted>
---
drivers/nvme/host/tcp.c | 61 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
@@ -1774,6 +1774,50 @@ static int nvme_tcp_start_tls(struct nvme_ctrl *nctrl,returnret;}+staticstructnet_device*nvme_tcp_get_netdev(structnvme_ctrl*ctrl,+netdevice_tracker*tracker,gfp_tgfp)+{+structnet_device*dev=NULL;++if(ctrl->opts->mask&NVMF_OPT_HOST_IFACE)+dev=netdev_get_by_name(&init_net,ctrl->opts->host_iface,+tracker,gfp);+else{+structnvme_tcp_ctrl*tctrl=to_tcp_ctrl(ctrl);+structsockaddr_storage*src=NULL,*dest=NULL;++if(ctrl->opts->mask&NVMF_OPT_HOST_TRADDR)+src=&tctrl->src_addr;++dest=&tctrl->addr;++dev=netdev_get_by_addr(&init_net,src,dest,tracker,gfp);+}+returndev;+}++/*+*ReturnsnumberofactiveNICqueues(minofTX/RX),or0ifdevicecannot+*bedetermined.+*/+staticintnvme_tcp_get_netdev_current_queue_count(structnvme_ctrl*ctrl)+{+structnet_device*dev;+inttx_queues,rx_queues;+netdevice_trackertracker;++dev=nvme_tcp_get_netdev(ctrl,&tracker,GFP_KERNEL);+if(!dev)+return0;++tx_queues=dev->real_num_tx_queues;+rx_queues=dev->real_num_rx_queues;++netdev_put(dev,&tracker);++returnmin(tx_queues,rx_queues);+}+staticintnvme_tcp_alloc_queue(structnvme_ctrl*nctrl,intqid,key_serial_tpskid){
@@ -2165,6 +2209,23 @@ static int nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl)unsignedintnr_io_queues;intret;
[..]
+ if (!(ctrl->opts->mask & NVMF_OPT_NR_IO_QUEUES)) {
+ int nr_hw_queues;
Looks like the userspace can already pass the preferred number of queues,
so in this case, why not do all this netdev resolution and queue
estimation in the userspace? Presumably most or the users you care
about always go through nvme-cli, right?
NVMe-TCP currently provisions I/O queues based primarily on the number
of online CPUs. On systems where the CPU count significantly exceeds the
number of NIC hardware queues, multiple NVMe-TCP I/O queues end up
sharing the same NIC TX/RX queues. This increases lock contention,
cacheline bouncing, and inter-processor interrupts (IPIs), reducing I/O
efficiency.
Limit the number of NVMe-TCP default I/O queues to the smaller of the
number of online CPUs and the number of NIC hardware queues. Aligning
the number of NVMe-TCP I/O queues with the NIC queue topology reduces
queue sharing, improves locality, and can improve throughput while
reducing tail latency.
The number of NVMe-TCP I/O queues is now limited to:
min(num_online_cpus, num_nic_queues)
Signed-off-by: Nilay Shroff <redacted>
---
drivers/nvme/host/tcp.c | 61 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
@@ -1774,6 +1774,50 @@ static int nvme_tcp_start_tls(struct nvme_ctrl *nctrl,returnret;}+staticstructnet_device*nvme_tcp_get_netdev(structnvme_ctrl*ctrl,+netdevice_tracker*tracker,gfp_tgfp)+{+structnet_device*dev=NULL;++if(ctrl->opts->mask&NVMF_OPT_HOST_IFACE)+dev=netdev_get_by_name(&init_net,ctrl->opts->host_iface,+tracker,gfp);+else{+structnvme_tcp_ctrl*tctrl=to_tcp_ctrl(ctrl);+structsockaddr_storage*src=NULL,*dest=NULL;++if(ctrl->opts->mask&NVMF_OPT_HOST_TRADDR)+src=&tctrl->src_addr;++dest=&tctrl->addr;++dev=netdev_get_by_addr(&init_net,src,dest,tracker,gfp);+}+returndev;+}++/*+*ReturnsnumberofactiveNICqueues(minofTX/RX),or0ifdevicecannot+*bedetermined.+*/+staticintnvme_tcp_get_netdev_current_queue_count(structnvme_ctrl*ctrl)+{+structnet_device*dev;+inttx_queues,rx_queues;+netdevice_trackertracker;++dev=nvme_tcp_get_netdev(ctrl,&tracker,GFP_KERNEL);+if(!dev)+return0;++tx_queues=dev->real_num_tx_queues;+rx_queues=dev->real_num_rx_queues;++netdev_put(dev,&tracker);++returnmin(tx_queues,rx_queues);+}+staticintnvme_tcp_alloc_queue(structnvme_ctrl*nctrl,intqid,key_serial_tpskid){
@@ -2165,6 +2209,23 @@ static int nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl)unsignedintnr_io_queues;intret;
[..]
quoted
+ if (!(ctrl->opts->mask & NVMF_OPT_NR_IO_QUEUES)) {
+ int nr_hw_queues;
Looks like the userspace can already pass the preferred number of queues,
so in this case, why not do all this netdev resolution and queue
estimation in the userspace? Presumably most or the users you care
about always go through nvme-cli, right?
Yes, userspace can already specify the preferred number of I/O queues, and nvme-cli
provides an option to do so when creating an NVMe/TCP connection. However, choosing
an appropriate value requires userspace to know both the number of online CPUs and
the number of active TX/RX queues on the NIC used for the connection. Determining
the latter also requires identifying the correct netdevice. That may involve a route
lookup to determine the egress interface, particularly when the NVMe/TCP host and
target are not on the same subnet.
So while this could be implemented in nvme-cli, it would require userspace to duplicate
the logic needed to determine the actual netdevice and its current queue configuration.
The intent of this change is to make the default queue selection automatic and avoid
requiring users to determine and specify this topology manually.
An explicitly specified "nr_io_queues" would still take precedence, so userspace can
override the default when desired.
Just for the note, this change also follows the general approach used by nvme-pci, where
the default number of I/O queues is constrained by both the number of possible CPUs and the
queue resources available from the controller.
Thanks,
--Nilay
Hi,
This is a resend of the previous series to include the networking maintainers
and mailing list. There are no code or commit message changes since the
previous posting.
This series has been updated based on the feedback received during LSFMM.
The changelog is updated accordingly.
The NVMe/TCP host driver currently provisions I/O queues primarily based
on CPU availability rather than the capabilities and topology of the
underlying network interface.
On modern systems with many CPUs but fewer NIC hardware queues, this can
lead to multiple NVMe/TCP I/O workers contending for the same TX/RX queue,
resulting in increased lock contention, cacheline bouncing, and degraded
throughput.
This RFC proposes a set of changes to better align NVMe/TCP I/O queues
with NIC queue resources, and to expose queue/flow information to enable
more effective system-level tuning.
Key ideas
---------
1. Scale NVMe/TCP I/O queues based on NIC queue count
Instead of relying solely on CPU count, limit the number of I/O workers
to:
min(num_online_cpus, netdev->real_num_{tx,rx}_queues)
2. Improve CPU locality
Align NVMe/TCP I/O workers with CPUs associated with NIC IRQ affinity
to reduce cross-CPU traffic and improve cache locality.
3. Expose queue and flow information via debugfs
Export per-I/O queue information including:
- queue id (qid)
- CPU affinity
- TCP flow (src/dst IP and ports)
[..]
quoted
This enables userspace tools to configure:
- IRQ affinity
- RPS/XPS
- ntuple steering
- or any other scaling as deemed feasible
Can you expand on this a bit? What specifically helped the most for your
tuned case?
For my network topology and workload, configuring ntuple steering together with
IRQ affinity and XPS provided the most benefit. The NIC used for testing supports
ntuple filters, which allowed me to steer the TCP flow associated with an NVMe/TCP
I/O queue to a specific NIC RX queue. I then configured the IRQ affinity for that
RX queue and XPS for the corresponding TX path so that the NVMe/TCP I/O queue
processing, RX interrupt processing, and TX/RX packet processing were aligned to
the same CPU as much as possible. This reduced cross-CPU processing for an I/O
flow and provided the best improvement among the tuning combinations I tested.
Thanks,
--Nilay
Just a nit, I don't think that raising a warning just because
kzalloc() returned NULL is a good idea as failures can naturally
occur under OOM conditions and aren't bugs.
Alright, I'll change it to just return -ENOMEM without producing
a warning, in case kzalloc fails.
Thanks,
--Nilay
From: Jakub Kicinski <kuba@kernel.org> Date: 2026-08-07 23:09:10
On Sat, 1 Aug 2026 19:08:05 +0530 Nilay Shroff wrote:
quoted
Looks like the userspace can already pass the preferred number of queues,
so in this case, why not do all this netdev resolution and queue
estimation in the userspace? Presumably most or the users you care
about always go through nvme-cli, right?
Yes, userspace can already specify the preferred number of I/O queues, and nvme-cli
provides an option to do so when creating an NVMe/TCP connection. However, choosing
an appropriate value requires userspace to know both the number of online CPUs and
the number of active TX/RX queues on the NIC used for the connection. Determining
the latter also requires identifying the correct netdevice. That may involve a route
lookup to determine the egress interface, particularly when the NVMe/TCP host and
target are not on the same subnet.
So while this could be implemented in nvme-cli, it would require userspace to duplicate
the logic needed to determine the actual netdevice and its current queue configuration.
The intent of this change is to make the default queue selection automatic and avoid
requiring users to determine and specify this topology manually.
In another message you said you add ntuple filters. So you _are_ doing
what you describe here as a problem. User space will know something we
don't know sooner or later, so you should just add the uAPI instead of
guessing in the kernel. BTW the queue count is likely to change after
all of user space boots, so if you run before whatever configures
queues for the machine in userspace you'll be using wrong counts.
An explicitly specified "nr_io_queues" would still take precedence, so userspace can
override the default when desired.
Just for the note, this change also follows the general approach used by nvme-pci, where
the default number of I/O queues is constrained by both the number of possible CPUs and the
queue resources available from the controller.
Not sure that maps well to networking. For TCP at least there will be
a protocol stack that runs between the device queues and your queues.
I guess that will depend on the network and the details of the
benchmark. But again, better to let the user tune to their workload
and machine.
Consider patch 1 nacked.
On Sat, 1 Aug 2026 19:08:05 +0530 Nilay Shroff wrote:
quoted
quoted
Looks like the userspace can already pass the preferred number of queues,
so in this case, why not do all this netdev resolution and queue
estimation in the userspace? Presumably most or the users you care
about always go through nvme-cli, right?
Yes, userspace can already specify the preferred number of I/O queues, and nvme-cli
provides an option to do so when creating an NVMe/TCP connection. However, choosing
an appropriate value requires userspace to know both the number of online CPUs and
the number of active TX/RX queues on the NIC used for the connection. Determining
the latter also requires identifying the correct netdevice. That may involve a route
lookup to determine the egress interface, particularly when the NVMe/TCP host and
target are not on the same subnet.
So while this could be implemented in nvme-cli, it would require userspace to duplicate
the logic needed to determine the actual netdevice and its current queue configuration.
The intent of this change is to make the default queue selection automatic and avoid
requiring users to determine and specify this topology manually.
In another message you said you add ntuple filters. So you _are_ doing
what you describe here as a problem. User space will know something we
don't know sooner or later, so you should just add the uAPI instead of
guessing in the kernel. BTW the queue count is likely to change after
all of user space boots, so if you run before whatever configures
queues for the machine in userspace you'll be using wrong counts.
Well, that ntuple filter configuration was done looking at the debugfs
output which is produced in patch 4/4. The debugfs generates the
enough information including queue count and per queue flow information
which is then programmed into ntuple filter.
quoted
An explicitly specified "nr_io_queues" would still take precedence, so userspace can
override the default when desired.
Just for the note, this change also follows the general approach used by nvme-pci, where
the default number of I/O queues is constrained by both the number of possible CPUs and the
queue resources available from the controller.
Not sure that maps well to networking. For TCP at least there will be
a protocol stack that runs between the device queues and your queues.
I guess that will depend on the network and the details of the
benchmark. But again, better to let the user tune to their workload
and machine.
Consider patch 1 nacked.
My motivation here was to improve the default behavior for the common case
where nr_io_queues is not explicitly specified. So if the preference is to
keep this in userspace, would you be open to exposing the required information
through a kernel interface (if something is still missing) and implementing
the queue selection logic in nvme-cli instead? That would still allow us to
automate the default queue selection without embedding this change in the kernel.
Thanks,
--Nilay