mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v3] nvme-tcp: allow setting per-queue io_cpu through sysfs
@ 2026-09-09 21:14 Saravanan D
  2026-09-10  8:25 ` Nilay Shroff
  2026-09-11 21:11 ` Sagi Grimberg
  0 siblings, 2 replies; 6+ messages in thread
From: Saravanan D @ 2026-09-09 21:14 UTC (permalink / raw)
  To: linux-nvme
  Cc: kbusch, hch, sagi, axboe, nilay, dwagner, linux-kernel,
	iyamahata, kiyer, ganbalagane, sj, Saravanan D

nvme_tcp_set_queue_io_cpu() picks each queue's io_cpu at connect time
as the least loaded CPU in the queue's blk-mq map group, and all socket
work runs there for the connection's lifetime. This decision falls
short when the host partitions its CPUs after connect time. On a 384
cpu multi tenant host with 128 queue controllers, blk-mq folds three
CPUs into every map group, some groups straddle two tenants' cpusets,
and 9% of nvme_tcp_io_work executions ran outside the submitting VM's
cpuset, seen by the neighbor as steal time.

Expose each I/O queue's io_cpu as a writable sysfs attribute

  /sys/class/nvme/nvmeX/tcp_queues/<qid>/io_cpu

so a control plane that owns CPU placement can set it directly instead
of relying on the driver's heuristic. A written value persists across
reconnects, marked by NVME_TCP_Q_IO_CPU_USER. Writing -1 clears the
mark and re-runs the connect time selection. Reading returns the CPU,
or -1 when the queue is unbound.

The store, the connect time selection and queue stop serialize their
accounting of nvme_tcp_cpu_queues under the queue lock.

Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/220e9da3-f756-4a16-8de1-d4b171f15009@grimberg.me/
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
Changes since v2 [1]:
- Replaced the io_cpu_adopt connect option and the submitter adoption
  heuristic with a per queue writable sysfs attribute, following Sagi's
  suggestion [2]. The control plane now sets each queue's io_cpu
  directly, the assignment is kept across reconnects and writing -1
  reverts to the connect time selection.
- Retitled from "nvme-tcp: pin io_cpu to submitter cpu".

The per queue directories follow the blk-mq mq/<hctx> sysfs pattern.

Tested on a 2 socket 384 cpu host with 128 queue controllers. Writing
a cpu number changed the queue's io_cpu to it. Writing an invalid
value was rejected. Writing -1 re-ran the connect time selection.
Pinned queues kept their io_cpu across a controller reset while
unpinned queues received a fresh pick.

The multiple queues per hctx RFC [3] found the same need to steer the
socket work cpu, so this attribute may gain a second user.

[1] https://lore.kernel.org/linux-nvme/20260820083634.71689-1-saravanand@crusoe.ai/
[2] https://lore.kernel.org/linux-nvme/1d56144d-6987-40c1-ac02-b15333db121e@grimberg.me/
[3] https://lore.kernel.org/linux-nvme/20260903152623.614951-1-kbusch@meta.com/

 Documentation/ABI/stable/sysfs-nvme |  12 +++
 drivers/nvme/host/tcp.c             | 153 +++++++++++++++++++++++++++-
 2 files changed, 162 insertions(+), 3 deletions(-)

diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme
index a2f5d0710db4..2bbb5a0b7c2e 100644
--- a/Documentation/ABI/stable/sysfs-nvme
+++ b/Documentation/ABI/stable/sysfs-nvme
@@ -451,3 +451,15 @@ Contact:	Hannes Reinecke <hare@suse.de>
 Description:
 		Shows the subsystem type. Possible values: "discovery",
 		"nvm", "reserved".
+
+What:		/sys/class/nvme/nvmeX/tcp_queues/<qid>/io_cpu
+Date:		September 2026
+KernelVersion:	7.4
+Contact:	Saravanan D <saravanand@crusoe.ai>
+Description:
+		(RW) The CPU that runs the socket work for I/O queue <qid>
+		of an NVMe over TCP controller, selected by the driver at
+		connect time. Writing a CPU number overrides the selection
+		and persists across reconnects. Writing -1 reverts to the
+		driver's selection. Reads show the current CPU, or -1 when
+		the queue is unbound.
diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
index 921934028e0b..22ad1fdf4a12 100644
--- a/drivers/nvme/host/tcp.c
+++ b/drivers/nvme/host/tcp.c
@@ -93,6 +93,7 @@ enum nvme_tcp_queue_flags {
 	NVME_TCP_Q_LIVE		= 1,
 	NVME_TCP_Q_POLLING	= 2,
 	NVME_TCP_Q_IO_CPU_SET	= 3,
+	NVME_TCP_Q_IO_CPU_USER	= 4,
 };
 
 enum nvme_tcp_recv_state {
@@ -102,6 +103,17 @@ enum nvme_tcp_recv_state {
 };
 
 struct nvme_tcp_ctrl;
+struct nvme_tcp_queue;
+
+/*
+ * Allocated per registration and freed by its kobject release, so a
+ * reconnect never reuses a kobject whose release is still pending.
+ */
+struct nvme_tcp_queue_kobj {
+	struct kobject		kobj;
+	struct nvme_tcp_queue	*queue;
+};
+
 struct nvme_tcp_queue {
 	struct socket		*sock;
 	struct work_struct	io_work;
@@ -141,6 +153,8 @@ struct nvme_tcp_queue {
 	int                     tls_err;
 	struct page_frag_cache	pf_cache;
 
+	struct nvme_tcp_queue_kobj *qkobj;
+
 	void (*state_change)(struct sock *);
 	void (*data_ready)(struct sock *);
 	void (*write_space)(struct sock *);
@@ -171,8 +185,11 @@ struct nvme_tcp_ctrl {
 	struct delayed_work	connect_work;
 	struct nvme_tcp_request async_req;
 	u32			io_queues[HCTX_MAX_TYPES];
+	struct kobject		*queues_kobj;
 };
 
+static void nvme_tcp_unregister_queue_sysfs(struct nvme_tcp_queue *queue);
+
 static struct workqueue_struct *nvme_tcp_wq;
 static const struct blk_mq_ops nvme_tcp_mq_ops;
 static const struct blk_mq_ops nvme_tcp_admin_mq_ops;
@@ -1497,6 +1514,8 @@ static void nvme_tcp_free_queue(struct nvme_ctrl *nctrl, int qid)
 	if (!test_and_clear_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
 		return;
 
+	nvme_tcp_unregister_queue_sysfs(queue);
+
 	page_frag_cache_drain(&queue->pf_cache);
 
 	/**
@@ -1716,9 +1735,19 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
 	unsigned int *mq_map = NULL;
 	int cpu, min_queues = INT_MAX, io_cpu;
 
+	lockdep_assert_held(&queue->queue_lock);
+
 	if (wq_unbound)
 		goto out;
 
+	/* A user assigned io_cpu is kept across reconnects */
+	if (test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags) &&
+	    queue->io_cpu != WORK_CPU_UNBOUND) {
+		if (!test_and_set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+			atomic_inc(&nvme_tcp_cpu_queues[queue->io_cpu]);
+		goto out;
+	}
+
 	if (nvme_tcp_default_queue(queue))
 		mq_map = set->map[HCTX_TYPE_DEFAULT].mq_map;
 	else if (nvme_tcp_read_queue(queue))
@@ -1836,6 +1865,118 @@ static int nvme_tcp_start_tls(struct nvme_ctrl *nctrl,
 	return ret;
 }
 
+static struct nvme_tcp_queue *nvme_tcp_kobj_to_queue(struct kobject *kobj)
+{
+	return container_of(kobj, struct nvme_tcp_queue_kobj, kobj)->queue;
+}
+
+static ssize_t io_cpu_show(struct kobject *kobj, struct kobj_attribute *attr,
+			   char *buf)
+{
+	struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
+	int io_cpu = READ_ONCE(queue->io_cpu);
+
+	return sysfs_emit(buf, "%d\n",
+			  io_cpu == WORK_CPU_UNBOUND ? -1 : io_cpu);
+}
+
+static ssize_t io_cpu_store(struct kobject *kobj, struct kobj_attribute *attr,
+			    const char *buf, size_t count)
+{
+	struct nvme_tcp_queue *queue = nvme_tcp_kobj_to_queue(kobj);
+	int cpu, old;
+	int ret;
+
+	ret = kstrtoint(buf, 0, &cpu);
+	if (ret)
+		return ret;
+	if (cpu != -1 &&
+	    ((unsigned int)cpu >= nr_cpu_ids || !cpu_online(cpu)))
+		return -EINVAL;
+
+	mutex_lock(&queue->queue_lock);
+	if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) {
+		mutex_unlock(&queue->queue_lock);
+		return -ENODEV;
+	}
+	if (cpu == -1) {
+		if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags)) {
+			if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET,
+					       &queue->flags))
+				atomic_dec(&nvme_tcp_cpu_queues[queue->io_cpu]);
+			WRITE_ONCE(queue->io_cpu, WORK_CPU_UNBOUND);
+			/* a queue that is not live gets its pick at start */
+			if (test_bit(NVME_TCP_Q_LIVE, &queue->flags))
+				nvme_tcp_set_queue_io_cpu(queue);
+		}
+	} else {
+		old = xchg(&queue->io_cpu, cpu);
+		set_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags);
+		if (test_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) {
+			atomic_dec(&nvme_tcp_cpu_queues[old]);
+			atomic_inc(&nvme_tcp_cpu_queues[cpu]);
+		}
+	}
+	mutex_unlock(&queue->queue_lock);
+
+	return count;
+}
+
+static struct kobj_attribute nvme_tcp_io_cpu_attr =
+	__ATTR(io_cpu, 0644, io_cpu_show, io_cpu_store);
+
+static struct attribute *nvme_tcp_queue_attrs[] = {
+	&nvme_tcp_io_cpu_attr.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(nvme_tcp_queue);
+
+static void nvme_tcp_queue_kobj_release(struct kobject *kobj)
+{
+	kfree(container_of(kobj, struct nvme_tcp_queue_kobj, kobj));
+}
+
+static const struct kobj_type nvme_tcp_queue_ktype = {
+	.sysfs_ops	= &kobj_sysfs_ops,
+	.release	= nvme_tcp_queue_kobj_release,
+	.default_groups	= nvme_tcp_queue_groups,
+};
+
+static void nvme_tcp_register_queue_sysfs(struct nvme_tcp_queue *queue)
+{
+	struct nvme_tcp_ctrl *ctrl = queue->ctrl;
+	struct nvme_tcp_queue_kobj *qkobj;
+
+	if (!ctrl->queues_kobj)
+		ctrl->queues_kobj = kobject_create_and_add("tcp_queues",
+							   &ctrl->ctrl.device->kobj);
+	if (!ctrl->queues_kobj)
+		return;
+
+	qkobj = kzalloc_obj(*qkobj);
+	if (!qkobj)
+		return;
+
+	qkobj->queue = queue;
+	if (kobject_init_and_add(&qkobj->kobj, &nvme_tcp_queue_ktype,
+				 ctrl->queues_kobj, "%d",
+				 nvme_tcp_queue_id(queue))) {
+		kobject_put(&qkobj->kobj);
+		return;
+	}
+	queue->qkobj = qkobj;
+}
+
+static void nvme_tcp_unregister_queue_sysfs(struct nvme_tcp_queue *queue)
+{
+	struct nvme_tcp_queue_kobj *qkobj = queue->qkobj;
+
+	if (!qkobj)
+		return;
+	queue->qkobj = NULL;
+	kobject_put(&qkobj->kobj);
+}
+
 static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
 				key_serial_t pskid)
 {
@@ -1906,7 +2047,8 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
 
 	queue->sock->sk->sk_allocation = GFP_ATOMIC;
 	queue->sock->sk->sk_use_task_frag = false;
-	queue->io_cpu = WORK_CPU_UNBOUND;
+	if (!test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags))
+		queue->io_cpu = WORK_CPU_UNBOUND;
 	queue->request = NULL;
 	queue->data_remaining = 0;
 	queue->ddgst_remaining = 0;
@@ -1974,6 +2116,9 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
 
 	set_bit(NVME_TCP_Q_ALLOCATED, &queue->flags);
 
+	if (qid)
+		nvme_tcp_register_queue_sysfs(queue);
+
 	return 0;
 
 err_init_connect:
@@ -2022,10 +2167,9 @@ static void nvme_tcp_stop_queue_nowait(struct nvme_ctrl *nctrl, int qid)
 	if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
 		return;
 
+	mutex_lock(&queue->queue_lock);
 	if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
 		atomic_dec(&nvme_tcp_cpu_queues[queue->io_cpu]);
-
-	mutex_lock(&queue->queue_lock);
 	if (test_and_clear_bit(NVME_TCP_Q_LIVE, &queue->flags))
 		__nvme_tcp_stop_queue(queue);
 	/* Stopping the queue will disable TLS */
@@ -2085,7 +2229,9 @@ static int nvme_tcp_start_queue(struct nvme_ctrl *nctrl, int idx)
 	nvme_tcp_setup_sock_ops(queue);
 
 	if (idx) {
+		mutex_lock(&queue->queue_lock);
 		nvme_tcp_set_queue_io_cpu(queue);
+		mutex_unlock(&queue->queue_lock);
 		ret = nvmf_connect_io_queue(nctrl, idx);
 	} else
 		ret = nvmf_connect_admin_queue(nctrl);
@@ -2650,6 +2796,7 @@ static void nvme_tcp_free_ctrl(struct nvme_ctrl *nctrl)
 
 	nvmf_free_options(nctrl->opts);
 free_ctrl:
+	kobject_put(ctrl->queues_kobj);
 	kfree(ctrl->queues);
 	kfree(ctrl);
 }

base-commit: fd9beb8870736e1c6a0b2351d88a161aaeb2b326
-- 
2.55.0


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

end of thread, other threads:[~2026-09-12 11:39 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-09 21:14 [PATCH v3] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-09-10  8:25 ` Nilay Shroff
2026-09-11  6:03   ` Saravanan D
2026-09-11 20:58     ` Sagi Grimberg
2026-09-11 21:11 ` Sagi Grimberg
2026-09-12 11:39   ` Nilay Shroff

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

all inboxes | Powered by JetHome®