mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Nilay Shroff <nilay@linux.ibm.com>
To: Saravanan D <saravanand@crusoe.ai>
Cc: linux-nvme@lists.infradead.org, kbusch@kernel.org, hch@lst.de,
	sagi@grimberg.me, axboe@kernel.dk, dwagner@suse.de,
	linux-kernel@vger.kernel.org, iyamahata@crusoe.ai,
	kiyer@crusoe.ai, ganbalagane@crusoe.ai, sjpark@crusoe.ai
Subject: Re: [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
Date: Mon, 12 Oct 2026 01:57:47 +0530	[thread overview]
Message-ID: <e3bd727d-869b-470d-84dc-fccae61ca499@linux.ibm.com> (raw)
In-Reply-To: <20261009020145.64651-1-saravanand@crusoe.ai>

On 10/9/26 7:31 AM, Saravanan D wrote:
> On 10/8/26 2:45 AM, Nilay Shroff wrote:
> [...]
>>> The queue directories register on the first connect and persist while
>>> the queues cycle across reconnects, so a write can arrive while a
>>> queue is torn down. The io_cpu changes and their accounting therefore
>>> serialize under a controller level lock rather than the queue lock,
>>> which is destroyed with the queue, and the controller teardown waits
>>> for the last kobject release before the queue array is freed.
>>>
>> I understand based on your requirement you may want to persist io_cpu
>> changes (when managed by user) during controller reconnect. However
>> in case num of queues changes after reconnect (assume it's 32 at first
>> connect but after controller reconnects it's reduced to 16) keeping
>> those 32 queues entries under queues/<qid>/ looks bit odd and not
>> correct. I'd expect queues entries under queue/<qid>/ to be also
>> adjusted based on the controller queue count.
> 
> Agreed. In v5 the queue directory follows the queue's connected
> lifetime. nvme_tcp_alloc_queue() adds it and nvme_tcp_free_queue()
> removes it, so queues/<qid>/ matches the queue count after every
> reconnect, as blk-mq does for mq/<n> when nr_hw_queues changes. A user
> assigned io_cpu is kept in the nvme_tcp_queue, which persists across
> reconnects, so it applies again when the queue comes back. A write
> while the controller is reconnecting fails with ENOENT.
> 
> [...]
>>> @@ -171,6 +173,11 @@ struct nvme_tcp_ctrl {
>>>    	struct delayed_work	connect_work;
>>>    	struct nvme_tcp_request async_req;
>>>    	u32			io_queues[HCTX_MAX_TYPES];
>>> +	/* serializes io_cpu changes and their nvme_tcp_cpu_queues accounting */
>>> +	struct mutex		io_cpu_lock;
>>
>> The io_cpu_lock protect updates to queue->io_cpu and as we now support clang
>> context annotations for NVMe host driver, I'd suggest you annotate the
>> queue->io_cpu with __guarded_by(...) so that clang context analyzer can
>> validate each access to queue->io_cpu. Furthermore, I see that queue->io_cpu
>> could be concurrently accessed from both control path and I/O hotpath. So how
>> would we protect it while it's being accessed from I/O path and concurrently
>> updated from sysfs path?
> 
> The I/O path reads io_cpu without the lock by design, and a stale read
> is benign. Each queue_work_on() call reads io_cpu once, so a call that
> races with the store queues io_work on the old cpu one last time and
> later calls use the new one. io_work is a single work item, so it
> never runs on both cpus at once. The inline send in
> nvme_tcp_queue_request() only compares io_cpu with the current cpu and
> is serialized by send_mutex. v5 marks the lockless reads with
> READ_ONCE() and the writes with WRITE_ONCE().
> 
> Annotating io_cpu with __guarded_by(&ctrl->io_cpu_lock) would need
> context_unsafe() around each of those hot path reads, so I'd rather
> document the lockless readers next to the field. Happy to add the
> annotation if you prefer it.

Yes, reading io_cpu locklessly in the I/O hot path is intentional. However,
to avoid KCSAN splat, we should mark read and write access with READ_ONCE()
and WRITE_ONCE() respectively.

Unfortunately, the Clang context analyzer cannot currently model a lock that
serializes writers while allowing readers to access the protected variable
without holding that lock. Annotating io_cpu with __guarded_by() would therefore
require us to annotate lockless reads with context_unsafe() or data_race(),
which I think we'd prefer to avoid.

I suggest introducing two helpers: one to read io_cpu and another to update it:

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(&queue->ctrl->io_cpu_lock)
{
	WRITE_ONCE(queue->io_cpu, cpu);
}

All readers could then access queue->io_cpu through nvme_tcp_queue_io_cpu(),
while all writes would go through nvme_tcp_queue_change_io_cpu(). The __must_hold()
annotation on the update helper would allow the Clang context analyzer to verify
that callers hold io_cpu_lock when modifying io_cpu, without requiring annotations
for the intentional lockless reads.

> 
>>> +	unsigned int		nr_queue_infos;
>>> +	atomic_t		qinfo_refs;
>>> +	struct completion	qinfo_release;
>>>    };
>>
>> I think the qinfo_refs and qinfo_release are added for tracking the
>> lifecycle of nvme_queue_info object. But do we really need to do
>> that ?
>> When a nvme_queue_info->kobj is initialized/added, its parent kobject
>> is referenced by the kobject infrastructure. Therefore, as long as a
>> queue-info kobject exists, its parent queues_kobj remains alive, which
>> in turn keeps ctrl->device->kobj alive. This should already prevent the
>> controller object from being released while a queue-info kobject still
>> exists. Could we therefore avoid maintaining a second qinfo_refs
>> reference count and completion? It seems that entire kobject parent
>> chain already provides the lifetime dependency that qinfo_refs is
>> trying to enforce.
> 
> Agreed. With the directory removed in nvme_tcp_free_queue(), no queue
> directory outlives its queue, and the kobject release only frees its
> own allocation, so the parent chain is enough. v5 drops qinfo_refs,
> the completion and nr_queue_infos. Each connection allocates its own
> kobject, so a reconnect never reuses one.
> 
> [...]
>> This look overly complicated. In a simple scheme can't we get rid off
>> NVME_QUEUE_INFO_IO_CPU_USER and instead use NVME_TCP_Q_IO_CPU_SET as an
>> indicator for differentiating between io_cpu is managed by user or driver?
>> For instance,
>>
>> NVME_TCP_Q_IO_CPU_SET = 1
>>       - driver has selected a specific io_cpu
>>
>> NVME_TCP_Q_IO_CPU_SET = 0 && io_cpu != WORK_CPU_UNBOUND
>>       - user has selected a specific io_cpu
>>
>> NVME_TCP_Q_IO_CPU_SET = 0 && io_cpu == WORK_CPU_UNBOUND
>>       - unbound; workqueue chooses the execution CPU and so it's driver managed
> 
> I drew your scheme as a state diagram to compare it with v4.
> 
>                DRIVER MANAGED          |         USER MANAGED
>     (SET=1, or SET=0 && UNBOUND)       |     (SET=0 && io_cpu=c)
>                                        |
>   connect +--------------------+ store(c)  +--------------------+
>   ------->| A  driver chosen   |-------+-->| C  user pin        |
>           |    io_cpu=c, SET=1 |       |   |    io_cpu=c, SET=0 |
>           |    counted         |       |   |    not counted (1) |
>           +--------------------+       |   +--------------------+
>             | stop, SET=0 and          |     |
>             | io_cpu must be reset (2) |     | store(-1)
>             v                          |     |
>           +--------------------+       |     |
>           | B  unbound         |<------+-----+
>           |    io_cpu=UNBOUND  |       |
>           |    SET=0           |       |   +- - - - - - - - - - +
>           |    also where a    |       |   : D  user unbound    :
>           |    user -1 lands(3)|       |   :    not reachable,  :
>           |                    |       |   :    see (3)         :
>           +--------------------+       |   +- - - - - - - - - - +
>             | next connect, reselect   |
>             +--> A                     |
> 
> (1) A user pin has SET=0, so it drops out of nvme_tcp_cpu_queues.
>      When the driver selects io_cpu for other queues and other
>      controllers in nvme_tcp_set_queue_io_cpu(), it sees that cpu as
>      idle and keeps placing queues on it. v4 counts every bound queue
>      regardless of who assigned it.
> 
> (2) SET clears at stop. Unless the stop path also resets io_cpu to
>      WORK_CPU_UNBOUND once io_work is cancelled, a driver chosen io_cpu
>      reads as a user pin after the reconnect and the driver never
>      selects a cpu for that queue again. In v4 only the sysfs store
>      sets the USER bit, so connect and stop never touch ownership.
> 
> (3) The user managed side has no unbound state. A -1 written by the
>      user is the same state as a queue the driver has not bound yet,
>      so managed reads 1 and the next connect binds it again. Also,
>      nvme_tcp_wq is a per-cpu workqueue, so an unbound io_work runs
>      on the cpu that queued it rather than on a cpu chosen by the
>      workqueue.
> 
May not be true always when nvme_tcp_wq is inited with wq_unbound. We
have a module option using which we could toggle this behavior.

> The USER bit records who owns io_cpu and survives reconnects, while
> SET records whether io_cpu is in the ledger and follows connect and
> stop. I'd like to keep both, with USER moved into nvme_tcp_queue flags
> as you asked on patch 1. The complexity you point at is the extra
> branch in the store, which collapses to a single path with the same
> behavior in v5.
> 
> 	mutex_lock(&ctrl->io_cpu_lock);
> 	old = xchg(&queue->io_cpu, cpu);
> 	set_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[old]);
> 	if (cpu != WORK_CPU_UNBOUND &&
> 	    test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) {
> 		atomic_inc(&nvme_tcp_cpu_queues[cpu]);
> 		set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
> 	}
> 	mutex_unlock(&ctrl->io_cpu_lock);
> 
> Does that work for you?
> 
This looks good. However, I wonder why we need to check NVME_TCP_Q_ALLOCATED
here? If the user can update io_cpu only while the queue's sysfs directory
exists, and that directory is created and removed along with the queue's
allocation and teardown, shouldn't we be able to drop this check?

>> Other that what sashiko provided in the review feedback, above are my
>> few additional comments.
> 
> Thanks for the review. v5 also fixes the Sashiko findings.
> 

Thanks,
--Nilay


  reply	other threads:[~2026-10-11 20:28 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-27  7:09 [PATCH v4 0/2] " Saravanan D
2026-09-27  7:09 ` [PATCH v4 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-10-08  9:59   ` Nilay Shroff
2026-09-27  7:09 ` [PATCH v4 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-08  9:45   ` Nilay Shroff
2026-10-09  2:01     ` Saravanan D
2026-10-11 20:27       ` Nilay Shroff [this message]
2026-10-08  5:39 ` [PATCH v4 0/2] " Saravanan D

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=e3bd727d-869b-470d-84dc-fccae61ca499@linux.ibm.com \
    --to=nilay@linux.ibm.com \
    --cc=axboe@kernel.dk \
    --cc=dwagner@suse.de \
    --cc=ganbalagane@crusoe.ai \
    --cc=hch@lst.de \
    --cc=iyamahata@crusoe.ai \
    --cc=kbusch@kernel.org \
    --cc=kiyer@crusoe.ai \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-nvme@lists.infradead.org \
    --cc=sagi@grimberg.me \
    --cc=saravanand@crusoe.ai \
    --cc=sjpark@crusoe.ai \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®