* [PATCH v8 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
@ 2026-11-07 9:53 Saravanan D
2026-11-07 9:53 ` [PATCH v8 1/2] nvme: add per-queue sysfs directories Saravanan D
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Saravanan D @ 2026-11-07 9:53 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen,
Saravanan D
On hosts that partition their CPUs after connect time, the connect
time io_cpu selection can place a queue's socket work on CPUs owned by
another workload. This series lets a control plane set each queue's
io_cpu through /sys/class/nvme/nvmeX/queues/<qid>/. Patch 1 adds the
core helpers, patch 2 the nvme-tcp attributes.
Changes since v7 [1]:
- nvme_add_queue_info() now calls kobject_del() when
sysfs_create_groups() fails, so the failed queue's <qid> directory
is removed before the error returns (Sashiko, Daniel).
- Rebased onto the current nvme-7.4.
The blktests patch covering the interface follows in this thread. It
is now nvme/081, since other pending blktests patches use nvme/076 to
nvme/080.
[1] https://lore.kernel.org/linux-nvme/cover.1793800758.git.saravanand@crusoe.ai/
Saravanan D (2):
nvme: add per-queue sysfs directories
nvme-tcp: allow setting per-queue io_cpu through sysfs
Documentation/ABI/stable/sysfs-nvme | 31 ++++++
drivers/nvme/host/core.c | 12 ++
drivers/nvme/host/nvme.h | 25 +++++
drivers/nvme/host/sysfs.c | 90 +++++++++++++++
drivers/nvme/host/tcp.c | 167 ++++++++++++++++++++++++----
5 files changed, 305 insertions(+), 20 deletions(-)
base-commit: 687f3014afcd5826fa3d8f40b445f647b5a379b6
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v8 1/2] nvme: add per-queue sysfs directories
2026-11-07 9:53 [PATCH v8 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-11-07 9:53 ` Saravanan D
2026-11-09 11:27 ` Nilay Shroff
2026-11-10 13:06 ` Hannes Reinecke
2026-11-07 9:53 ` [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-11-07 9:53 ` [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute Saravanan D
2 siblings, 2 replies; 12+ messages in thread
From: Saravanan D @ 2026-11-07 9:53 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen,
Saravanan D
Per I/O queue transport state, such as the nvme-tcp io_cpu, has
nowhere to live in sysfs, since the core has no directory for
individual queues.
Add helpers that create the controller's queues directory and add or
remove one directory per queue
/sys/class/nvme/nvmeX/queues/<qid>/
carrying the attributes a transport sets in the new queue_attr_groups
member of struct nvme_ctrl_ops. For such transports nvme_add_ctrl()
creates the queues directory. It holds a reference on the controller
device, so nvme_uninit_ctrl() deletes it before deleting the device,
after the transport has removed the queue directories.
The transport adds a queue's directory when it sets the queue up and
removes it when it frees the queue, so the directories follow the
queues across reconnects and changes in the queue count. The core
allocates each queue directory and its release frees only that
allocation. Removal waits for running attribute callbacks, so the
transport may tear the queue down once it returns.
Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
drivers/nvme/host/core.c | 12 ++++++
drivers/nvme/host/nvme.h | 25 +++++++++++
drivers/nvme/host/sysfs.c | 90 +++++++++++++++++++++++++++++++++++++++
3 files changed, 127 insertions(+)
diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index cd0df9b03806..66e1e2c614e8 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -5208,6 +5208,8 @@ void nvme_uninit_ctrl(struct nvme_ctrl *ctrl)
*/
cancel_work_sync(&ctrl->async_event_work);
nvme_hwmon_exit(ctrl);
+ /* transports remove their queue directories before this point */
+ nvme_queue_dir_destroy(ctrl);
nvme_fault_inject_fini(&ctrl->fault_inject);
dev_pm_qos_hide_latency_tolerance(ctrl->device);
cdev_device_del(&ctrl->cdev, ctrl->device);
@@ -5352,6 +5354,12 @@ int nvme_add_ctrl(struct nvme_ctrl *ctrl)
if (ret)
return ret;
+ if (ctrl->ops->queue_attr_groups) {
+ ret = nvme_queue_dir_create(ctrl);
+ if (ret)
+ goto out_del_cdev;
+ }
+
/*
* Initialize latency tolerance controls. The sysfs files won't
* be visible to userspace unless the device actually supports APST.
@@ -5364,6 +5372,10 @@ int nvme_add_ctrl(struct nvme_ctrl *ctrl)
nvme_get_ctrl(ctrl);
return 0;
+
+out_del_cdev:
+ cdev_device_del(&ctrl->cdev, ctrl->device);
+ return ret;
}
EXPORT_SYMBOL_GPL(nvme_add_ctrl);
diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h
index 2543e52d6358..95b598c71b4a 100644
--- a/drivers/nvme/host/nvme.h
+++ b/drivers/nvme/host/nvme.h
@@ -356,6 +356,28 @@ enum nvme_ctrl_flags {
NVME_CTRL_FROZEN = 6,
};
+/*
+ * Per-queue directory /sys/class/nvme/nvmeX/queues/<qid>/, allocated by the
+ * core and carrying attributes provided by the transport.
+ */
+struct nvme_queue_info {
+ struct kobject kobj;
+ void *data;
+};
+
+static inline void *nvme_queue_info_data(struct kobject *kobj)
+{
+ return container_of(kobj, struct nvme_queue_info, kobj)->data;
+}
+
+struct nvme_ctrl;
+
+int nvme_queue_dir_create(struct nvme_ctrl *ctrl);
+void nvme_queue_dir_destroy(struct nvme_ctrl *ctrl);
+struct nvme_queue_info *
+nvme_add_queue_info(struct nvme_ctrl *ctrl, unsigned int qid, void *data);
+void nvme_remove_queue_info(struct nvme_queue_info *qinfo);
+
struct nvme_ctrl {
bool comp_seen;
bool identified;
@@ -377,6 +399,7 @@ struct nvme_ctrl {
struct srcu_struct srcu;
struct device ctrl_device;
struct device *device; /* char device */
+ struct kobject *queues_kobj;
#ifdef CONFIG_NVME_HWMON
struct device *hwmon_device;
#endif
@@ -675,6 +698,8 @@ struct nvme_ctrl_ops {
#define NVME_F_BLOCKING (1 << 2)
const struct attribute_group **dev_attr_groups;
+ /* attributes of each /sys/class/nvme/nvmeX/queues/<qid>/ directory */
+ const struct attribute_group **queue_attr_groups;
int (*reg_read32)(struct nvme_ctrl *ctrl, u32 off, u32 *val);
int (*reg_write32)(struct nvme_ctrl *ctrl, u32 off, u32 val);
int (*reg_read64)(struct nvme_ctrl *ctrl, u32 off, u64 *val);
diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c
index 3724b5ce03a0..ff81030f161c 100644
--- a/drivers/nvme/host/sysfs.c
+++ b/drivers/nvme/host/sysfs.c
@@ -1311,3 +1311,93 @@ const struct attribute_group *nvme_subsys_attrs_groups[] = {
&nvme_subsys_attrs_group,
NULL,
};
+
+static void nvme_queue_info_release(struct kobject *kobj)
+{
+ kfree(container_of(kobj, struct nvme_queue_info, kobj));
+}
+
+static const struct kobj_type nvme_queue_info_ktype = {
+ .sysfs_ops = &kobj_sysfs_ops,
+ .release = nvme_queue_info_release,
+};
+
+/*
+ * nvme_add_ctrl() creates /sys/class/nvme/nvmeX/queues/ for transports that
+ * provide queue attributes, and nvme_uninit_ctrl() destroys it.
+ */
+int nvme_queue_dir_create(struct nvme_ctrl *ctrl)
+{
+ ctrl->queues_kobj = kobject_create_and_add("queues",
+ &ctrl->device->kobj);
+ if (!ctrl->queues_kobj)
+ return -ENOMEM;
+ return 0;
+}
+
+void nvme_queue_dir_destroy(struct nvme_ctrl *ctrl)
+{
+ if (!ctrl->queues_kobj)
+ return;
+
+ kobject_del(ctrl->queues_kobj);
+ kobject_put(ctrl->queues_kobj);
+ ctrl->queues_kobj = NULL;
+}
+
+/**
+ * nvme_add_queue_info - add a directory for one queue
+ * @ctrl: controller, whose queues directory must exist
+ * @qid: queue id, used as the directory name
+ * @data: transport data, returned by nvme_queue_info_data()
+ *
+ * Adds /sys/class/nvme/nvmeX/queues/<qid>/ carrying the transport's
+ * queue_attr_groups. Calls for different queues of
+ * the same controller may run concurrently. Returns the queue info, to be
+ * passed to nvme_remove_queue_info(), or an ERR_PTR().
+ */
+struct nvme_queue_info *
+nvme_add_queue_info(struct nvme_ctrl *ctrl, unsigned int qid, void *data)
+{
+ struct nvme_queue_info *qinfo;
+ int ret;
+
+ if (WARN_ON_ONCE(!ctrl->queues_kobj))
+ return ERR_PTR(-EINVAL);
+
+ qinfo = kzalloc_obj(*qinfo);
+ if (!qinfo)
+ return ERR_PTR(-ENOMEM);
+ qinfo->data = data;
+
+ ret = kobject_init_and_add(&qinfo->kobj, &nvme_queue_info_ktype,
+ ctrl->queues_kobj, "%u", qid);
+ if (ret)
+ goto out_put;
+
+ ret = sysfs_create_groups(&qinfo->kobj, ctrl->ops->queue_attr_groups);
+ if (ret)
+ goto out_del;
+ return qinfo;
+
+out_del:
+ kobject_del(&qinfo->kobj);
+out_put:
+ kobject_put(&qinfo->kobj);
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_GPL(nvme_add_queue_info);
+
+/**
+ * nvme_remove_queue_info - remove a queue directory
+ * @qinfo: queue info returned by nvme_add_queue_info()
+ *
+ * Waits for running attribute callbacks, so none run once this returns
+ * and the transport data may be torn down.
+ */
+void nvme_remove_queue_info(struct nvme_queue_info *qinfo)
+{
+ kobject_del(&qinfo->kobj);
+ kobject_put(&qinfo->kobj);
+}
+EXPORT_SYMBOL_GPL(nvme_remove_queue_info);
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-11-07 9:53 [PATCH v8 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-11-07 9:53 ` [PATCH v8 1/2] nvme: add per-queue sysfs directories Saravanan D
@ 2026-11-07 9:53 ` Saravanan D
2026-11-09 11:40 ` Nilay Shroff
2026-11-10 13:09 ` Hannes Reinecke
2026-11-07 9:53 ` [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute Saravanan D
2 siblings, 2 replies; 12+ messages in thread
From: Saravanan D @ 2026-11-07 9:53 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen,
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 the io_cpu in each I/O queue's directory
/sys/class/nvme/nvmeX/queues/<qid>/io_cpu
so a control plane that owns CPU placement can set it directly instead
of relying on the driver's heuristic. The attribute accepts a decimal
cpu number, -1 or "unbound". A user assignment, unbound included, is
marked in the queue flags, persists across reconnects and is never
reselected by the driver. The read-only managed attribute reports 1
while io_cpu is driver managed and 0 once the user assigned it. The
accounting of nvme_tcp_cpu_queues applies regardless of who set the
io_cpu.
A queue's directory is added once the connect time selection has set its
io_cpu, so readers never see a placement that is about to change, and
removed when the queue is freed, before it is torn down, so a sysfs
store never runs on a freed queue. All io_cpu writes, from
sysfs and from the driver, serialize with the accounting under
nvme_tcp_cpu_queues_lock, while the I/O paths read io_cpu locklessly.
The queue stop now clears NVME_TCP_Q_IO_CPU_SET under that lock, since a
sysfs store may move io_cpu in between.
nvme_tcp_free_queue() now clears NVME_TCP_Q_IO_CPU_SET and drops its
count, because a queue can still be counted when it is freed. A sysfs
store may have run after the queue stop, or the connect may have
failed, and the setup error path frees such a queue without calling
nvme_tcp_stop_queue(), where the count is normally dropped.
Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
Documentation/ABI/stable/sysfs-nvme | 31 ++++++
drivers/nvme/host/tcp.c | 167 ++++++++++++++++++++++++----
2 files changed, 178 insertions(+), 20 deletions(-)
diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme
index 1826df9161d2..b9d1d0f87686 100644
--- a/Documentation/ABI/stable/sysfs-nvme
+++ b/Documentation/ABI/stable/sysfs-nvme
@@ -502,3 +502,34 @@ Contact: Hannes Reinecke <hare@suse.de>
Description:
Shows the subsystem type. Possible values: "discovery",
"nvm", "reserved".
+
+What: /sys/class/nvme/nvmeX/queues/<qid>/io_cpu
+What: /sys/class/nvme/nvmeX/queues/<qid>/managed
+Date: November 2026
+KernelVersion: 7.5
+Contact: Saravanan D <saravanand@crusoe.ai>
+Description:
+ Per I/O queue directories, populated by the transport
+ backing the controller. A directory appears once the
+ driver has selected the queue's CPU at connect time and
+ is removed when the queue is torn down, so the
+ directories follow the queue count and are absent while
+ the controller reconnects.
+ NVMe over TCP exposes:
+
+ io_cpu: (RW) The CPU that runs the socket work for I/O
+ queue <qid>, selected by the driver at connect time.
+ Writing a decimal online CPU number overrides the
+ selection, and writing -1 or "unbound" leaves the socket
+ work unbound. A written value persists across reconnects
+ for the life of the controller, also when its CPU goes
+ offline. Unbound socket work runs on the CPU that queued
+ it. When the nvme_tcp module is loaded with wq_unbound
+ set, the socket work runs on an unbound workqueue and
+ how closely it follows io_cpu depends on the affinity
+ scope of the nvme_tcp_wq workqueue. Reads return the
+ current CPU, or -1 when the queue is unbound.
+
+ managed: (RO) Shows who owns the CPU placement. 1 while
+ io_cpu is driver managed, 0 once the user assigned it,
+ for the life of the controller.
diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
index 2587db92a3a2..b681fb44ce42 100644
--- a/drivers/nvme/host/tcp.c
+++ b/drivers/nvme/host/tcp.c
@@ -117,6 +117,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,
};
#define NVME_TCP_OPT_NR_QUEUES(opts) ((opts)->nr_io_queues + \
@@ -167,6 +168,7 @@ struct nvme_tcp_queue {
struct completion tls_complete;
int tls_err;
struct page_frag_cache pf_cache;
+ struct nvme_queue_info *qinfo;
void (*state_change)(struct sock *);
void (*data_ready)(struct sock *);
@@ -256,6 +258,19 @@ static inline int nvme_tcp_queue_id(struct nvme_tcp_queue *queue)
return queue - queue->ctrl->queues;
}
+/* the I/O paths read io_cpu locklessly, writers hold the accounting lock */
+static inline int nvme_tcp_queue_io_cpu(struct nvme_tcp_queue *queue)
+{
+ return READ_ONCE(queue->io_cpu);
+}
+
+static inline void nvme_tcp_queue_change_io_cpu(struct nvme_tcp_queue *queue,
+ int cpu)
+ __must_hold(&nvme_tcp_cpu_queues_lock)
+{
+ WRITE_ONCE(queue->io_cpu, cpu);
+}
+
static inline bool nvme_tcp_recv_pdu_supported(enum nvme_tcp_pdu_type type)
{
switch (type) {
@@ -453,7 +468,7 @@ static inline void nvme_tcp_queue_request(struct nvme_tcp_request *req,
* lockdep reports circular locking via elevator_lock. Defer TLS
* sends to the io workqueue instead of inline from this path.
*/
- if (queue->io_cpu == raw_smp_processor_id() &&
+ if (nvme_tcp_queue_io_cpu(queue) == raw_smp_processor_id() &&
!nvme_tcp_queue_tls(queue) &&
empty && mutex_trylock(&queue->send_mutex)) {
nvme_tcp_send_all(queue);
@@ -461,7 +476,8 @@ static inline void nvme_tcp_queue_request(struct nvme_tcp_request *req,
}
if (last && nvme_tcp_queue_has_pending(queue))
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
static void nvme_tcp_process_req_list(struct nvme_tcp_queue *queue)
@@ -870,7 +886,8 @@ static int nvme_tcp_handle_r2t(struct nvme_tcp_queue *queue,
nvme_tcp_setup_h2c_data_pdu(req);
llist_add(&req->lentry, &queue->req_list);
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
return 0;
}
@@ -1157,7 +1174,8 @@ static void nvme_tcp_data_ready(struct sock *sk)
queue = sk->sk_user_data;
if (likely(queue && queue->rd_enabled) &&
!test_bit(NVME_TCP_Q_POLLING, &queue->flags))
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
read_unlock_bh(&sk->sk_callback_lock);
}
@@ -1172,7 +1190,8 @@ static void nvme_tcp_write_space(struct sock *sk)
/* Ensure pending TLS partial records are retried */
if (nvme_tcp_queue_tls(queue))
queue->write_space(sk);
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
read_unlock_bh(&sk->sk_callback_lock);
}
@@ -1515,7 +1534,8 @@ static void nvme_tcp_io_work(struct work_struct *w)
} while (!time_after(jiffies, deadline)); /* quota is exhausted */
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
static void nvme_tcp_free_async_req(struct nvme_tcp_ctrl *ctrl)
@@ -1549,9 +1569,22 @@ static void nvme_tcp_free_queue(struct nvme_ctrl *nctrl, int qid)
struct nvme_tcp_queue *queue = &ctrl->queues[qid];
unsigned int noio_flag;
- if (!test_and_clear_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
+ if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
return;
+ /* no io_cpu store runs once the directory is removed */
+ if (queue->qinfo) {
+ nvme_remove_queue_info(queue->qinfo);
+ queue->qinfo = NULL;
+ }
+ clear_bit(NVME_TCP_Q_ALLOCATED, &queue->flags);
+
+ /* a failed connect or a store after the stop left it counted */
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[nvme_tcp_queue_io_cpu(queue)]--;
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
+
page_frag_cache_drain(&queue->pf_cache);
/**
@@ -1883,8 +1916,18 @@ 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;
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ /* a user assigned io_cpu is kept across reconnects */
+ if (test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags)) {
+ io_cpu = nvme_tcp_queue_io_cpu(queue);
+ if (io_cpu != WORK_CPU_UNBOUND &&
+ !test_and_set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[io_cpu]++;
+ goto out_unlock;
+ }
+
if (wq_unbound)
- goto out;
+ goto out_unlock;
if (nvme_tcp_default_queue(queue))
mq_map = set->map[HCTX_TYPE_DEFAULT].mq_map;
@@ -1894,10 +1937,9 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
mq_map = set->map[HCTX_TYPE_POLL].mq_map;
if (WARN_ON(!mq_map))
- goto out;
+ goto out_unlock;
/* Search for the least used cpu from the mq_map */
- spin_lock(&nvme_tcp_cpu_queues_lock);
io_cpu = WORK_CPU_UNBOUND;
for_each_online_cpu(cpu) {
int num_queues = nvme_tcp_cpu_queues[cpu];
@@ -1910,14 +1952,14 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
}
}
if (io_cpu != WORK_CPU_UNBOUND) {
- queue->io_cpu = io_cpu;
+ nvme_tcp_queue_change_io_cpu(queue, io_cpu);
nvme_tcp_cpu_queues[io_cpu]++;
set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
}
+out_unlock:
spin_unlock(&nvme_tcp_cpu_queues_lock);
-out:
dev_dbg(ctrl->ctrl.device, "queue %d: using cpu %d\n",
- qid, queue->io_cpu);
+ qid, nvme_tcp_queue_io_cpu(queue));
}
static void nvme_tcp_tls_done(void *data, int status, key_serial_t pskid)
@@ -2030,6 +2072,84 @@ static void nvme_tcp_exit_queue(struct nvme_tcp_ctrl *ctrl, int qid)
mutex_destroy(&queue->pf_cache_lock);
}
+static ssize_t io_cpu_show(struct kobject *kobj, struct kobj_attribute *attr,
+ char *buf)
+{
+ struct nvme_tcp_queue *queue = nvme_queue_info_data(kobj);
+ int io_cpu = nvme_tcp_queue_io_cpu(queue);
+
+ 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_queue_info_data(kobj);
+ int cpu, old, ret;
+
+ if (sysfs_streq(buf, "unbound")) {
+ cpu = WORK_CPU_UNBOUND;
+ } else {
+ ret = kstrtoint(buf, 10, &cpu);
+ if (ret)
+ return ret;
+ if (cpu == -1)
+ cpu = WORK_CPU_UNBOUND;
+ else if ((unsigned int)cpu >= nr_cpu_ids || !cpu_online(cpu))
+ return -EINVAL;
+ }
+
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ old = nvme_tcp_queue_io_cpu(queue);
+ nvme_tcp_queue_change_io_cpu(queue, cpu);
+ set_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[old]--;
+ if (cpu != WORK_CPU_UNBOUND) {
+ nvme_tcp_cpu_queues[cpu]++;
+ set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
+ }
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
+
+ return count;
+}
+
+static ssize_t managed_show(struct kobject *kobj, struct kobj_attribute *attr,
+ char *buf)
+{
+ struct nvme_tcp_queue *queue = nvme_queue_info_data(kobj);
+
+ return sysfs_emit(buf, "%d\n",
+ !test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags));
+}
+
+static struct kobj_attribute nvme_tcp_io_cpu_attr =
+ __ATTR(io_cpu, 0644, io_cpu_show, io_cpu_store);
+static struct kobj_attribute nvme_tcp_managed_attr =
+ __ATTR_RO(managed);
+
+static struct attribute *nvme_tcp_queue_attrs[] = {
+ &nvme_tcp_io_cpu_attr.attr,
+ &nvme_tcp_managed_attr.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(nvme_tcp_queue);
+
+static void nvme_tcp_add_queue_sysfs(struct nvme_tcp_queue *queue)
+{
+ struct nvme_ctrl *nctrl = &queue->ctrl->ctrl;
+ struct nvme_queue_info *qinfo;
+
+ qinfo = nvme_add_queue_info(nctrl, nvme_tcp_queue_id(queue), queue);
+ if (IS_ERR(qinfo)) {
+ dev_warn(nctrl->device, "queue %d: no sysfs directory: %ld\n",
+ nvme_tcp_queue_id(queue), PTR_ERR(qinfo));
+ return;
+ }
+ queue->qinfo = qinfo;
+}
+
static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
key_serial_t pskid)
{
@@ -2087,7 +2207,11 @@ 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;
+ /* no store races here, the lock is for the write helper */
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ if (!test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags))
+ nvme_tcp_queue_change_io_cpu(queue, WORK_CPU_UNBOUND);
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
queue->request = NULL;
queue->data_remaining = 0;
queue->ddgst_remaining = 0;
@@ -2194,11 +2318,10 @@ static void nvme_tcp_stop_queue_nowait(struct nvme_ctrl *nctrl, int qid)
if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
return;
- if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) {
- spin_lock(&nvme_tcp_cpu_queues_lock);
- nvme_tcp_cpu_queues[queue->io_cpu]--;
- spin_unlock(&nvme_tcp_cpu_queues_lock);
- }
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[nvme_tcp_queue_io_cpu(queue)]--;
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
mutex_lock(&queue->queue_lock);
if (test_and_clear_bit(NVME_TCP_Q_LIVE, &queue->flags))
@@ -2261,6 +2384,8 @@ static int nvme_tcp_start_queue(struct nvme_ctrl *nctrl, int idx)
if (idx) {
nvme_tcp_set_queue_io_cpu(queue);
+ /* expose io_cpu only once the driver has selected it */
+ nvme_tcp_add_queue_sysfs(queue);
ret = nvmf_connect_io_queue(nctrl, idx);
} else
ret = nvmf_connect_admin_queue(nctrl);
@@ -3057,7 +3182,8 @@ static void nvme_tcp_commit_rqs(struct blk_mq_hw_ctx *hctx)
struct nvme_tcp_queue *queue = hctx->driver_data;
if (!llist_empty(&queue->req_list))
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
static blk_status_t nvme_tcp_queue_rq(struct blk_mq_hw_ctx *hctx,
@@ -3160,6 +3286,7 @@ static const struct nvme_ctrl_ops nvme_tcp_ctrl_ops = {
.name = "tcp",
.module = THIS_MODULE,
.flags = NVME_F_FABRICS | NVME_F_BLOCKING,
+ .queue_attr_groups = nvme_tcp_queue_groups,
.reg_read32 = nvmf_reg_read32,
.reg_read64 = nvmf_reg_read64,
.reg_write32 = nvmf_reg_write32,
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute
2026-11-07 9:53 [PATCH v8 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-11-07 9:53 ` [PATCH v8 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-11-07 9:53 ` [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-11-07 9:53 ` Saravanan D
2026-11-09 13:43 ` Nilay Shroff
2 siblings, 1 reply; 12+ messages in thread
From: Saravanan D @ 2026-11-07 9:53 UTC (permalink / raw)
To: linux-block
Cc: shinichiro.kawasaki, linux-nvme, dwagner, hare, nilay, sagi,
kbusch, iyamahata, kiyer, ganbalagane, sjpark, bchen, Saravanan D
nvme-tcp exposes /sys/class/nvme/nvmeX/queues/<qid>/io_cpu and managed,
so a control plane can choose the CPU that runs each I/O queue's socket
work [1]. Test that
- io_cpu reads back a written CPU and rejects invalid values
- managed is read only and turns 0 once io_cpu is written
- an assignment survives a controller reset
- the queue directories follow the queue count as attr_qid_max
shrinks and grows it
- removing the controller while io_cpu is written completes
The test skips when the kernel does not have the attribute.
[1] https://lore.kernel.org/linux-nvme/cover.1794015622.git.saravanand@crusoe.ai/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
This is the nvme/077 test posted with v7 [2], renumbered because other
pending blktests patches use nvme/076 to nvme/080. The test itself is
unchanged.
[2] https://lore.kernel.org/all/20261104140721.4489-1-saravanand@crusoe.ai/
tests/nvme/081 | 185 +++++++++++++++++++++++++++++++++++++++++++++
tests/nvme/081.out | 2 +
2 files changed, 187 insertions(+)
create mode 100755 tests/nvme/081
create mode 100644 tests/nvme/081.out
diff --git a/tests/nvme/081 b/tests/nvme/081
new file mode 100755
index 0000000..0b4bce5
--- /dev/null
+++ b/tests/nvme/081
@@ -0,0 +1,185 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Saravanan D <saravanand@crusoe.ai>
+#
+# Test the per-queue io_cpu sysfs attribute of nvme-tcp
+
+. tests/nvme/rc
+
+DESCRIPTION="test the per-queue io_cpu sysfs attribute"
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _require_nvme_trtype tcp
+ _require_min_cpus 2
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+# last CPU in the online list, and the first CPU id past the possible ones
+last_online_cpu() {
+ awk -F'[-,]' '{ print $NF }' /sys/devices/system/cpu/online
+}
+
+first_impossible_cpu() {
+ echo $(( $(awk -F'[-,]' '{ print $NF }' /sys/devices/system/cpu/possible) + 1 ))
+}
+
+nr_queue_dirs() {
+ local nvmedev="$1"
+
+ find "/sys/class/nvme/${nvmedev}/queues" -mindepth 1 -maxdepth 1 \
+ -type d 2>/dev/null | wc -l
+}
+
+expect_attr() {
+ local file="$1"
+ local expected="$2"
+ local value
+
+ value=$(cat "${file}")
+ if [[ "${value}" != "${expected}" ]]; then
+ echo "${file}: read ${value}, expected ${expected}"
+ return 1
+ fi
+ return 0
+}
+
+test_read_write() {
+ local qdir="$1"
+ local cpu="$2"
+ local bad
+
+ expect_attr "${qdir}/1/managed" 1
+
+ echo "${cpu}" > "${qdir}/1/io_cpu"
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ expect_attr "${qdir}/1/managed" 0
+
+ for bad in "$(first_impossible_cpu)" -2 abc 0x1; do
+ if { echo "${bad}" > "${qdir}/1/io_cpu"; } 2>/dev/null; then
+ echo "io_cpu accepted ${bad}"
+ fi
+ done
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+
+ if { echo 1 > "${qdir}/1/managed"; } 2>/dev/null; then
+ echo "managed is writable"
+ fi
+
+ echo unbound > "${qdir}/2/io_cpu"
+ expect_attr "${qdir}/2/io_cpu" -1
+ echo -1 > "${qdir}/2/io_cpu"
+ expect_attr "${qdir}/2/io_cpu" -1
+ expect_attr "${qdir}/2/managed" 0
+}
+
+test_reset() {
+ local nvmedev="$1"
+ local qdir="$2"
+ local cpu="$3"
+
+ echo 1 > "/sys/class/nvme/${nvmedev}/reset_controller"
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ expect_attr "${qdir}/1/managed" 0
+ expect_attr "${qdir}/2/io_cpu" -1
+ expect_attr "${qdir}/2/managed" 0
+}
+
+set_qid_max() {
+ local nvmedev="$1"
+ local qid_max="$2"
+ local queue_count_file="/sys/class/nvme-fabrics/ctl/${nvmedev}/queue_count"
+ local retries=10
+
+ echo "${qid_max}" > \
+ "${NVMET_CFS}/subsystems/${def_subsysnqn}/attr_qid_max"
+ while [[ "$(cat "${queue_count_file}")" -ne $((qid_max + 1)) ]]; do
+ if (( retries-- == 0 )); then
+ echo "queue count did not change to $((qid_max + 1))"
+ return 1
+ fi
+ sleep 1
+ done
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ if [[ "$(nr_queue_dirs "${nvmedev}")" -ne "${qid_max}" ]]; then
+ echo "$(nr_queue_dirs "${nvmedev}") queue directories," \
+ "expected ${qid_max}"
+ return 1
+ fi
+ return 0
+}
+
+io_cpu_writer() {
+ local qdir="$1"
+ local cpu="$2"
+ local file
+
+ while true; do
+ for file in "${qdir}"/*/io_cpu; do
+ { echo "${cpu}" > "${file}"; } 2>/dev/null
+ { echo unbound > "${file}"; } 2>/dev/null
+ done
+ done
+}
+
+test() {
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+
+ local cfs_path="${NVMET_CFS}/subsystems/${def_subsysnqn}"
+ local skipped=false
+ local nvmedev qdir cpu writer
+
+ _nvmet_target_setup --blkdev file
+
+ _nvme_connect_subsys --keep-alive-tmo 1 --reconnect-delay 2
+ _nvmf_wait_for_state "${def_subsysnqn}" "live"
+ nvmedev=$(_find_nvme_dev "${def_subsysnqn}")
+ qdir="/sys/class/nvme/${nvmedev}/queues"
+ cpu=$(last_online_cpu)
+
+ if [[ ! -f "${qdir}/1/io_cpu" ]]; then
+ SKIP_REASONS+=("missing per-queue io_cpu attribute")
+ skipped=true
+ elif [[ "$(nr_queue_dirs "${nvmedev}")" -lt 2 ]]; then
+ SKIP_REASONS+=("controller has fewer than 2 I/O queues")
+ skipped=true
+ else
+ test_read_write "${qdir}" "${cpu}"
+ test_reset "${nvmedev}" "${qdir}" "${cpu}"
+
+ if [[ -f "${cfs_path}/attr_qid_max" ]]; then
+ set_qid_max "${nvmedev}" 1
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ set_qid_max "${nvmedev}" 2
+ fi
+
+ # remove the controller while its queue directories are written
+ io_cpu_writer "${qdir}" "${cpu}" &
+ writer=$!
+ sleep 1
+ fi
+
+ _nvme_disconnect_subsys
+
+ if [[ -n "${writer}" ]]; then
+ kill "${writer}"
+ wait "${writer}" 2>/dev/null
+ fi
+
+ _nvmet_target_cleanup
+
+ if [[ "${skipped}" = true ]] ; then
+ return 1
+ fi
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/081.out b/tests/nvme/081.out
new file mode 100644
index 0000000..5deb638
--- /dev/null
+++ b/tests/nvme/081.out
@@ -0,0 +1,2 @@
+Running nvme/081
+Test complete
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v8 1/2] nvme: add per-queue sysfs directories
2026-11-07 9:53 ` [PATCH v8 1/2] nvme: add per-queue sysfs directories Saravanan D
@ 2026-11-09 11:27 ` Nilay Shroff
2026-11-10 13:06 ` Hannes Reinecke
1 sibling, 0 replies; 12+ messages in thread
From: Nilay Shroff @ 2026-11-09 11:27 UTC (permalink / raw)
To: Saravanan D, linux-nvme
Cc: kbusch, hch, sagi, axboe, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen
On 11/7/26 3:23 PM, Saravanan D wrote:
> Per I/O queue transport state, such as the nvme-tcp io_cpu, has
> nowhere to live in sysfs, since the core has no directory for
> individual queues.
>
> Add helpers that create the controller's queues directory and add or
> remove one directory per queue
>
> /sys/class/nvme/nvmeX/queues/<qid>/
>
> carrying the attributes a transport sets in the new queue_attr_groups
> member of struct nvme_ctrl_ops. For such transports nvme_add_ctrl()
> creates the queues directory. It holds a reference on the controller
> device, so nvme_uninit_ctrl() deletes it before deleting the device,
> after the transport has removed the queue directories.
>
> The transport adds a queue's directory when it sets the queue up and
> removes it when it frees the queue, so the directories follow the
> queues across reconnects and changes in the queue count. The core
> allocates each queue directory and its release frees only that
> allocation. Removal waits for running attribute callbacks, so the
> transport may tear the queue down once it returns.
>
> Suggested-by: Sagi Grimberg<sagi@grimberg.me>
> Link:https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
> Signed-off-by: Saravanan D<saravanand@crusoe.ai>
Looks good to me:
Reviewed-by: Nilay Shroff <nilay@linux.ibm.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-11-07 9:53 ` [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-11-09 11:40 ` Nilay Shroff
2026-11-10 13:09 ` Hannes Reinecke
1 sibling, 0 replies; 12+ messages in thread
From: Nilay Shroff @ 2026-11-09 11:40 UTC (permalink / raw)
To: Saravanan D, linux-nvme
Cc: kbusch, hch, sagi, axboe, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen
On 11/7/26 3:23 PM, Saravanan D wrote:
> 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 the io_cpu in each I/O queue's directory
>
> /sys/class/nvme/nvmeX/queues/<qid>/io_cpu
>
> so a control plane that owns CPU placement can set it directly instead
> of relying on the driver's heuristic. The attribute accepts a decimal
> cpu number, -1 or "unbound". A user assignment, unbound included, is
> marked in the queue flags, persists across reconnects and is never
> reselected by the driver. The read-only managed attribute reports 1
> while io_cpu is driver managed and 0 once the user assigned it. The
> accounting of nvme_tcp_cpu_queues applies regardless of who set the
> io_cpu.
>
> A queue's directory is added once the connect time selection has set its
> io_cpu, so readers never see a placement that is about to change, and
> removed when the queue is freed, before it is torn down, so a sysfs
> store never runs on a freed queue. All io_cpu writes, from
> sysfs and from the driver, serialize with the accounting under
> nvme_tcp_cpu_queues_lock, while the I/O paths read io_cpu locklessly.
> The queue stop now clears NVME_TCP_Q_IO_CPU_SET under that lock, since a
> sysfs store may move io_cpu in between.
>
> nvme_tcp_free_queue() now clears NVME_TCP_Q_IO_CPU_SET and drops its
> count, because a queue can still be counted when it is freed. A sysfs
> store may have run after the queue stop, or the connect may have
> failed, and the setup error path frees such a queue without calling
> nvme_tcp_stop_queue(), where the count is normally dropped.
>
> Suggested-by: Sagi Grimberg<sagi@grimberg.me>
> Link:https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
> Signed-off-by: Saravanan D<saravanand@crusoe.ai>
Looks good to me:
Reviewed-by: Nilay Shroff <nilay@linux.ibm.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute
2026-11-07 9:53 ` [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute Saravanan D
@ 2026-11-09 13:43 ` Nilay Shroff
2026-11-10 11:49 ` Saravanan D
2026-11-10 11:50 ` [PATCH blktests v2] " Saravanan D
0 siblings, 2 replies; 12+ messages in thread
From: Nilay Shroff @ 2026-11-09 13:43 UTC (permalink / raw)
To: Saravanan D, linux-block
Cc: shinichiro.kawasaki, linux-nvme, dwagner, hare, sagi, kbusch,
iyamahata, kiyer, ganbalagane, sjpark, bchen
On 11/7/26 3:23 PM, Saravanan D wrote:
> +test() {
> + echo "Running ${TEST_NAME}"
> +
> + _setup_nvmet
> +
> + local cfs_path="${NVMET_CFS}/subsystems/${def_subsysnqn}"
> + local skipped=false
> + local nvmedev qdir cpu writer
> +
> + _nvmet_target_setup --blkdev file
> +
> + _nvme_connect_subsys --keep-alive-tmo 1 --reconnect-delay 2
How about adding connection option "--nr-io-queues 2"? If we add
this option then we may not need to have the requirement of
_require_min_cpus 2. I think, we shall be able to run this test
also on the system with only one cpu.
> + _nvmf_wait_for_state "${def_subsysnqn}" "live"
> + nvmedev=$(_find_nvme_dev "${def_subsysnqn}")
> + qdir="/sys/class/nvme/${nvmedev}/queues"
> + cpu=$(last_online_cpu)
> +
Can we also add one more test here which tests whether "$qdir"
exists or not?
> + if [[ ! -f "${qdir}/1/io_cpu" ]]; then
> + SKIP_REASONS+=("missing per-queue io_cpu attribute")
> + skipped=true
> + elif [[ "$(nr_queue_dirs "${nvmedev}")" -lt 2 ]]; then
> + SKIP_REASONS+=("controller has fewer than 2 I/O queues")
> + skipped=true
I think using --nr-io-queues option would also replace the above relative
comparison with an exact comparison based on num of queues selected.
> + else
> + test_read_write "${qdir}" "${cpu}"
> + test_reset "${nvmedev}" "${qdir}" "${cpu}"
The reset test could be updated to ensure that number of queues
remains unchanged after controller reset. Current test_reset()
verifies that the io_cpu and managed attributes retain their expected
values after reset. However, it doesn't explicitly verify that the
queue directories were recreated with the same count. I would capture
the queue_count before reset and compare it afterward as well as
ensure that queue_count matches with nr_queue_dirs.
Thanks,
--Nilay
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute
2026-11-09 13:43 ` Nilay Shroff
@ 2026-11-10 11:49 ` Saravanan D
2026-11-10 11:50 ` [PATCH blktests v2] " Saravanan D
1 sibling, 0 replies; 12+ messages in thread
From: Saravanan D @ 2026-11-10 11:49 UTC (permalink / raw)
To: Nilay Shroff
Cc: linux-block, shinichiro.kawasaki, linux-nvme, dwagner, hare, sagi,
kbusch, iyamahata, kiyer, ganbalagane, sjpark, bchen
On Mon, Nov 09, 2026 at 07:13:27PM +0530, Nilay Shroff wrote:
> > + _nvme_connect_subsys --keep-alive-tmo 1 --reconnect-delay 2
>
> How about adding connection option "--nr-io-queues 2"? If we add
> this option then we may not need to have the requirement of
> _require_min_cpus 2. I think, we shall be able to run this test
> also on the system with only one cpu.
I added --nr-io-queues 2, which pins the queue count. The test still
needs _require_min_cpus 2 though. nvmf_parse_options() caps
nr_io_queues at num_online_cpus(), so a single CPU host still gets one
I/O queue, and the test needs queues 1 and 2.
> Can we also add one more test here which tests whether "$qdir"
> exists or not?
Yes. v2 checks the queues directory and the io_cpu attribute
separately, with a skip reason for each, so a kernel that has only the
first patch of the series reports which part is missing.
> I think using --nr-io-queues option would also replace the above relative
> comparison with an exact comparison based on num of queues selected.
Yes. The test now expects exactly two queue directories after connect,
and a different count fails the test instead of skipping it.
> The reset test could be updated to ensure that number of queues
> remains unchanged after controller reset. Current test_reset()
> verifies that the io_cpu and managed attributes retain their expected
> values after reset. However, it doesn't explicitly verify that the
> queue directories were recreated with the same count. I would capture
> the queue_count before reset and compare it afterward as well as
> ensure that queue_count matches with nr_queue_dirs.
v2 reads queue_count before the reset and checks that it is unchanged
afterwards. queue_count includes the admin queue, so the test then
checks that the number of queue directories is queue_count - 1.
I will send v2 in reply to this mail. Thanks also for reviewing both
kernel patches.
Thanks,
Saravanan D.
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH blktests v2] nvme/081: test the per-queue io_cpu sysfs attribute
2026-11-09 13:43 ` Nilay Shroff
2026-11-10 11:49 ` Saravanan D
@ 2026-11-10 11:50 ` Saravanan D
2026-11-11 8:47 ` Nilay Shroff
1 sibling, 1 reply; 12+ messages in thread
From: Saravanan D @ 2026-11-10 11:50 UTC (permalink / raw)
To: linux-block
Cc: nilay, shinichiro.kawasaki, linux-nvme, dwagner, hare, sagi,
kbusch, iyamahata, kiyer, ganbalagane, sjpark, bchen
nvme-tcp exposes /sys/class/nvme/nvmeX/queues/<qid>/io_cpu and managed,
so a control plane can choose the CPU that runs each I/O queue's socket
work [1]. Test that
- io_cpu reads back a written CPU and rejects invalid values
- managed is read only and turns 0 once io_cpu is written
- an assignment and the queue directories survive a controller reset
- the queue directories follow the queue count as attr_qid_max
shrinks and grows it
- removing the controller while io_cpu is written completes
The test skips when the kernel does not have the queues/ directory or
the io_cpu attribute.
[1] https://lore.kernel.org/linux-nvme/cover.1794015622.git.saravanand@crusoe.ai/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
Changes since v1 [2]:
- Connect with --nr-io-queues 2 and expect exactly two queue
directories (Nilay).
- Skip with separate reasons when the queues directory or the io_cpu
attribute is missing (Nilay).
- Check that queue_count is unchanged across a controller reset and
that the queue directories match it (Nilay).
[2] https://lore.kernel.org/all/20261107095350.98072-1-saravanand@crusoe.ai/
tests/nvme/081 | 208 +++++++++++++++++++++++++++++++++++++++++++++
tests/nvme/081.out | 2 +
2 files changed, 210 insertions(+)
create mode 100755 tests/nvme/081
create mode 100644 tests/nvme/081.out
diff --git a/tests/nvme/081 b/tests/nvme/081
new file mode 100755
index 0000000..0bda988
--- /dev/null
+++ b/tests/nvme/081
@@ -0,0 +1,208 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Saravanan D <saravanand@crusoe.ai>
+#
+# Test the per-queue io_cpu sysfs attribute of nvme-tcp
+
+. tests/nvme/rc
+
+DESCRIPTION="test the per-queue io_cpu sysfs attribute"
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _require_nvme_trtype tcp
+ _require_min_cpus 2
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+# last CPU in the online list, and the first CPU id past the possible ones
+last_online_cpu() {
+ awk -F'[-,]' '{ print $NF }' /sys/devices/system/cpu/online
+}
+
+first_impossible_cpu() {
+ echo $(( $(awk -F'[-,]' '{ print $NF }' /sys/devices/system/cpu/possible) + 1 ))
+}
+
+nr_queue_dirs() {
+ local nvmedev="$1"
+
+ find "/sys/class/nvme/${nvmedev}/queues" -mindepth 1 -maxdepth 1 \
+ -type d 2>/dev/null | wc -l
+}
+
+# queue_count includes the admin queue
+queue_count() {
+ cat "/sys/class/nvme-fabrics/ctl/$1/queue_count"
+}
+
+expect_queue_dirs() {
+ local nvmedev="$1"
+ local expected="$2"
+ local nr
+
+ nr=$(nr_queue_dirs "${nvmedev}")
+ if [[ "${nr}" -ne "${expected}" ]]; then
+ echo "${nr} queue directories, expected ${expected}"
+ return 1
+ fi
+ return 0
+}
+
+expect_attr() {
+ local file="$1"
+ local expected="$2"
+ local value
+
+ value=$(cat "${file}")
+ if [[ "${value}" != "${expected}" ]]; then
+ echo "${file}: read ${value}, expected ${expected}"
+ return 1
+ fi
+ return 0
+}
+
+test_read_write() {
+ local qdir="$1"
+ local cpu="$2"
+ local bad
+
+ expect_attr "${qdir}/1/managed" 1
+
+ echo "${cpu}" > "${qdir}/1/io_cpu"
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ expect_attr "${qdir}/1/managed" 0
+
+ for bad in "$(first_impossible_cpu)" -2 abc 0x1; do
+ if { echo "${bad}" > "${qdir}/1/io_cpu"; } 2>/dev/null; then
+ echo "io_cpu accepted ${bad}"
+ fi
+ done
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+
+ if { echo 1 > "${qdir}/1/managed"; } 2>/dev/null; then
+ echo "managed is writable"
+ fi
+
+ echo unbound > "${qdir}/2/io_cpu"
+ expect_attr "${qdir}/2/io_cpu" -1
+ echo -1 > "${qdir}/2/io_cpu"
+ expect_attr "${qdir}/2/io_cpu" -1
+ expect_attr "${qdir}/2/managed" 0
+}
+
+test_reset() {
+ local nvmedev="$1"
+ local qdir="$2"
+ local cpu="$3"
+ local count
+
+ count=$(queue_count "${nvmedev}")
+ echo 1 > "/sys/class/nvme/${nvmedev}/reset_controller"
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ if [[ "$(queue_count "${nvmedev}")" -ne "${count}" ]]; then
+ echo "queue count $(queue_count "${nvmedev}") after reset," \
+ "expected ${count}"
+ fi
+ expect_queue_dirs "${nvmedev}" $((count - 1))
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ expect_attr "${qdir}/1/managed" 0
+ expect_attr "${qdir}/2/io_cpu" -1
+ expect_attr "${qdir}/2/managed" 0
+}
+
+set_qid_max() {
+ local nvmedev="$1"
+ local qid_max="$2"
+ local retries=10
+
+ echo "${qid_max}" > \
+ "${NVMET_CFS}/subsystems/${def_subsysnqn}/attr_qid_max"
+ while [[ "$(queue_count "${nvmedev}")" -ne $((qid_max + 1)) ]]; do
+ if (( retries-- == 0 )); then
+ echo "queue count did not change to $((qid_max + 1))"
+ return 1
+ fi
+ sleep 1
+ done
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ expect_queue_dirs "${nvmedev}" "${qid_max}"
+}
+
+io_cpu_writer() {
+ local qdir="$1"
+ local cpu="$2"
+ local file
+
+ while true; do
+ for file in "${qdir}"/*/io_cpu; do
+ { echo "${cpu}" > "${file}"; } 2>/dev/null
+ { echo unbound > "${file}"; } 2>/dev/null
+ done
+ done
+}
+
+test() {
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+
+ local cfs_path="${NVMET_CFS}/subsystems/${def_subsysnqn}"
+ local skipped=false
+ local nvmedev qdir cpu writer
+
+ _nvmet_target_setup --blkdev file
+
+ # fabrics caps nr_io_queues at the online CPU count, hence the
+ # _require_min_cpus 2
+ _nvme_connect_subsys --keep-alive-tmo 1 --reconnect-delay 2 \
+ --nr-io-queues 2
+ _nvmf_wait_for_state "${def_subsysnqn}" "live"
+ nvmedev=$(_find_nvme_dev "${def_subsysnqn}")
+ qdir="/sys/class/nvme/${nvmedev}/queues"
+ cpu=$(last_online_cpu)
+
+ if [[ ! -d "${qdir}" ]]; then
+ SKIP_REASONS+=("missing per-queue sysfs directories")
+ skipped=true
+ elif [[ ! -f "${qdir}/1/io_cpu" ]]; then
+ SKIP_REASONS+=("missing per-queue io_cpu attribute")
+ skipped=true
+ else
+ expect_queue_dirs "${nvmedev}" 2
+ test_read_write "${qdir}" "${cpu}"
+ test_reset "${nvmedev}" "${qdir}" "${cpu}"
+
+ if [[ -f "${cfs_path}/attr_qid_max" ]]; then
+ set_qid_max "${nvmedev}" 1
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ set_qid_max "${nvmedev}" 2
+ fi
+
+ # remove the controller while its queue directories are written
+ io_cpu_writer "${qdir}" "${cpu}" &
+ writer=$!
+ sleep 1
+ fi
+
+ _nvme_disconnect_subsys
+
+ if [[ -n "${writer}" ]]; then
+ kill "${writer}"
+ wait "${writer}" 2>/dev/null
+ fi
+
+ _nvmet_target_cleanup
+
+ if [[ "${skipped}" = true ]] ; then
+ return 1
+ fi
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/081.out b/tests/nvme/081.out
new file mode 100644
index 0000000..5deb638
--- /dev/null
+++ b/tests/nvme/081.out
@@ -0,0 +1,2 @@
+Running nvme/081
+Test complete
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v8 1/2] nvme: add per-queue sysfs directories
2026-11-07 9:53 ` [PATCH v8 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-11-09 11:27 ` Nilay Shroff
@ 2026-11-10 13:06 ` Hannes Reinecke
1 sibling, 0 replies; 12+ messages in thread
From: Hannes Reinecke @ 2026-11-10 13:06 UTC (permalink / raw)
To: Saravanan D, linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen
On 11/7/26 10:53 AM, Saravanan D wrote:
> Per I/O queue transport state, such as the nvme-tcp io_cpu, has
> nowhere to live in sysfs, since the core has no directory for
> individual queues.
>
> Add helpers that create the controller's queues directory and add or
> remove one directory per queue
>
> /sys/class/nvme/nvmeX/queues/<qid>/
>
> carrying the attributes a transport sets in the new queue_attr_groups
> member of struct nvme_ctrl_ops. For such transports nvme_add_ctrl()
> creates the queues directory. It holds a reference on the controller
> device, so nvme_uninit_ctrl() deletes it before deleting the device,
> after the transport has removed the queue directories.
>
> The transport adds a queue's directory when it sets the queue up and
> removes it when it frees the queue, so the directories follow the
> queues across reconnects and changes in the queue count. The core
> allocates each queue directory and its release frees only that
> allocation. Removal waits for running attribute callbacks, so the
> transport may tear the queue down once it returns.
>
> Suggested-by: Sagi Grimberg <sagi@grimberg.me>
> Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
> Signed-off-by: Saravanan D <saravanand@crusoe.ai>
> ---
> drivers/nvme/host/core.c | 12 ++++++
> drivers/nvme/host/nvme.h | 25 +++++++++++
> drivers/nvme/host/sysfs.c | 90 +++++++++++++++++++++++++++++++++++++++
> 3 files changed, 127 insertions(+)
>
Reviewed-by: Hannes Reinecke <hare@kernel.org>
Cheers,
Hannes
--
Dr. Hannes Reinecke Kernel Storage Architect
hare@suse.de +49 911 74053 688
SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg
HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-11-07 9:53 ` [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-11-09 11:40 ` Nilay Shroff
@ 2026-11-10 13:09 ` Hannes Reinecke
1 sibling, 0 replies; 12+ messages in thread
From: Hannes Reinecke @ 2026-11-10 13:09 UTC (permalink / raw)
To: Saravanan D, linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen
On 11/7/26 10:53 AM, Saravanan D wrote:
> 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 the io_cpu in each I/O queue's directory
>
> /sys/class/nvme/nvmeX/queues/<qid>/io_cpu
>
> so a control plane that owns CPU placement can set it directly instead
> of relying on the driver's heuristic. The attribute accepts a decimal
> cpu number, -1 or "unbound". A user assignment, unbound included, is
> marked in the queue flags, persists across reconnects and is never
> reselected by the driver. The read-only managed attribute reports 1
> while io_cpu is driver managed and 0 once the user assigned it. The
> accounting of nvme_tcp_cpu_queues applies regardless of who set the
> io_cpu.
>
> A queue's directory is added once the connect time selection has set its
> io_cpu, so readers never see a placement that is about to change, and
> removed when the queue is freed, before it is torn down, so a sysfs
> store never runs on a freed queue. All io_cpu writes, from
> sysfs and from the driver, serialize with the accounting under
> nvme_tcp_cpu_queues_lock, while the I/O paths read io_cpu locklessly.
> The queue stop now clears NVME_TCP_Q_IO_CPU_SET under that lock, since a
> sysfs store may move io_cpu in between.
>
> nvme_tcp_free_queue() now clears NVME_TCP_Q_IO_CPU_SET and drops its
> count, because a queue can still be counted when it is freed. A sysfs
> store may have run after the queue stop, or the connect may have
> failed, and the setup error path frees such a queue without calling
> nvme_tcp_stop_queue(), where the count is normally dropped.
>
> Suggested-by: Sagi Grimberg <sagi@grimberg.me>
> Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
> Signed-off-by: Saravanan D <saravanand@crusoe.ai>
> ---
> Documentation/ABI/stable/sysfs-nvme | 31 ++++++
> drivers/nvme/host/tcp.c | 167 ++++++++++++++++++++++++----
> 2 files changed, 178 insertions(+), 20 deletions(-)
>
Reviewed-by: Hannes Reinecke <hare@kernel.org>
Cheers,
Hannes
--
Dr. Hannes Reinecke Kernel Storage Architect
hare@suse.de +49 911 74053 688
SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg
HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH blktests v2] nvme/081: test the per-queue io_cpu sysfs attribute
2026-11-10 11:50 ` [PATCH blktests v2] " Saravanan D
@ 2026-11-11 8:47 ` Nilay Shroff
0 siblings, 0 replies; 12+ messages in thread
From: Nilay Shroff @ 2026-11-11 8:47 UTC (permalink / raw)
To: Saravanan D, linux-block
Cc: shinichiro.kawasaki, linux-nvme, dwagner, hare, sagi, kbusch,
iyamahata, kiyer, ganbalagane, sjpark, bchen
On 11/10/26 5:20 PM, Saravanan D wrote:
> nvme-tcp exposes/sys/class/nvme/nvmeX/queues/<qid>/io_cpu and managed,
> so a control plane can choose the CPU that runs each I/O queue's socket
> work [1]. Test that
>
> - io_cpu reads back a written CPU and rejects invalid values
> - managed is read only and turns 0 once io_cpu is written
> - an assignment and the queue directories survive a controller reset
> - the queue directories follow the queue count as attr_qid_max
> shrinks and grows it
> - removing the controller while io_cpu is written completes
>
> The test skips when the kernel does not have the queues/ directory or
> the io_cpu attribute.
>
> [1]https://lore.kernel.org/linux-nvme/cover.1794015622.git.saravanand@crusoe.ai/
>
> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
> Signed-off-by: Saravanan D<saravanand@crusoe.ai>
Looks good to me:
Reviewed-by: Nilay Shroff <nilay@linux.ibm.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-11-11 8:48 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-07 9:53 [PATCH v8 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-11-07 9:53 ` [PATCH v8 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-11-09 11:27 ` Nilay Shroff
2026-11-10 13:06 ` Hannes Reinecke
2026-11-07 9:53 ` [PATCH v8 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-11-09 11:40 ` Nilay Shroff
2026-11-10 13:09 ` Hannes Reinecke
2026-11-07 9:53 ` [PATCH blktests] nvme/081: test the per-queue io_cpu sysfs attribute Saravanan D
2026-11-09 13:43 ` Nilay Shroff
2026-11-10 11:49 ` Saravanan D
2026-11-10 11:50 ` [PATCH blktests v2] " Saravanan D
2026-11-11 8:47 ` Nilay Shroff
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox