[PATCH v7 4/9] nvme-multipath: add support for latency I/O policy

John Garry john.g.garry at oracle.com
Tue Aug 11 03:03:28 PDT 2026


>>> +}
>>> +
>>> +/*
>>> + * Formula to calculate the EWMA (Exponentially Weighted Moving Average):
>>> + * ewma = (old_ewma * (EWMA_SHIFT - 1) + (EWMA_SHIFT)) / EWMA_SHIFT
>>> + * For instance, with EWMA_SHIFT = 3, this assigns 7/8 (~87.5 %) weight to
>>> + * the existing/old ewma and 1/8 (~12.5%) weight to the new sample.
>>> + */
>>> +static inline u64 calc_ewma_update(u64 old, u64 new)
>>> +{
>>> +    return (old * ((1 << NVME_DEFAULT_LATENCY_EWMA_SHIFT) - 1)
>>> +            + new) >> NVME_DEFAULT_LATENCY_EWMA_SHIFT;

side note: I have to admit that I did not check all the mathematics of 
these ewma calculations ...

>>> +}
>>> +
>>> +static void nvme_mpath_add_sample(struct request *rq, struct nvme_ns *ns)

Could the context analysis annotation be added here eventually to 
declare that the srcu read lock is held?

>>> +{
>>> +    int cpu;
>>> +    unsigned int op_type;
>>> +    struct nvme_path_lat *path_lat;
>>> +    struct nvme_path_lat_stat *stat;
>>> +    u64 now, latency, slat_ns, avg_lat_ns;
>>> +    struct nvme_ns_head *head = ns->head;
>>> +
>>> +    if (list_is_singular(&head->list))
>>> +        return;
>>> +
>>> +    now = ktime_get_ns();
>>> +    latency = now >= rq->io_start_time_ns ? now - rq->io_start_time_ns : 0;
>>> +    if (!latency)
>>> +        return;
>>> +
>>> +    /*
>>> +     * As completion code path is serialized(i.e. no same completion queue
>>> +     * update code could run simultaneously on multiple cpu) we can safely
>>> +     * access per cpu nvme path stat here from another cpu (in case the
>>> +     * completion cpu is different from submission cpu).
>>> +     * The only field which could be accessed simultaneously here is the
>>> +     * path ->weight which may be accessed by this function as well as I/O
>>> +     * submission path during path selection logic and we protect ->weight
>>> +     * using READ_ONCE/WRITE_ONCE. Yes this may not be 100% accurate but
>>> +     * we also don't need to be so accurate here as the path credit would
>>> +     * be anyways refilled, based on path weight, once path consumes all
>>> +     * its credits. And we limit path weight/credit max up to 64. Please
>>> +     * also refer nvme_latency_path().
>>> +     */

...

>>>    void nvme_mpath_end_request(struct request *rq)
>>>    {
>>>        struct nvme_ns *ns = rq->q->queuedata;
>>> @@ -206,6 +407,15 @@ void nvme_mpath_end_request(struct request *rq)
>>>        if (nvme_req(rq)->flags & NVME_MPATH_CNT_ACTIVE)
>>>            atomic_dec_if_positive(&ns->ctrl->nr_active);
>>> +    if (test_bit(NVME_NS_PATH_STAT, &ns->flags)) {
>>> +        int srcu_idx;
>>> +
>>> +        srcu_idx = srcu_read_lock(&ns->head->srcu);
>>> +        if (test_bit(NVME_NS_PATH_STAT, &ns->flags))
>> 
>> Some may ask why check NVME_NS_PATH_STAT twice.
> 
> The first check is a fast-path optimization to avoid taking the SRCU read
> lock when latency sampling is disabled. The second check is needed because
> NVME_NS_PATH_STAT could be cleared after the first test but before acquiring
> the SRCU lock, so we revalidate it after entering the protected section.

It is probably worth a brief comment on that. A similar trick is done in 
__blk_mq_tag_busy() and every so often someone asks about it. Or maybe 
it is another function. I don't remember.

>> 
>>> +    blk_stat_enable_accounting(ns->queue);
>>> +    return true;
>>> +}
>>> +
>>> +static bool nvme_disable_ns_latency_sampling(struct nvme_ns *ns)
>>> +{
>>> +    int cpu;
>>> +    struct nvme_ns_head *head = ns->head;
>>> +    bool changed = false;
>>> +
>>> +    if (!test_and_clear_bit(NVME_NS_PATH_STAT, &ns->flags))
>>> +        return false;
>>> +
>>> +    for_each_possible_cpu(cpu) {
>>> +        if (ns == READ_ONCE(*per_cpu_ptr(head->latency_path, cpu))) {
>>> +            WRITE_ONCE(*per_cpu_ptr(head->latency_path, cpu), NULL);
>>> +            changed = true;
>>> +        }
>>> +    }
>>> +
>>> +    blk_stat_disable_accounting(ns->queue);
>>> +    blk_queue_flag_clear(QUEUE_FLAG_SAME_FORCE, ns->queue);
>> 
>> eh, what if QUEUE_FLAG_SAME_FORCE was already enabled before nvme_enable_ns_latency_sampling()?
>> 
> Good catch! It looks like we need a nested reference count for
> QUEUE_FLAG_SAME_FORCE, similar to QUEUE_FLAG_STATS and
> QUEUE_FLAG_QUIESCED.

Furthermore, I think that userspace can change this via sysfs, no? I 
think that the file is rq_affinity. If so, could that break things (if 
userspace did change this flag)?

>>>    }
>>> @@ -268,6 +554,45 @@ void nvme_mpath_clear_ctrl_paths(struct nvme_ctrl *ctrl)
>>>        srcu_read_unlock(&ctrl->srcu, srcu_idx);
>>>    }
>>> +int nvme_alloc_ns_stat(struct nvme_ns *ns)
>> 
>> Surely "mpath" should be in the name, no? It seems that every other public API in multpath.c has "mpath" in the name.
> 
> Not all APIs have "mpath" in its name, such as nvme_failover_req(),
> nvme_kick_requeue_lists() etc, but most other have. So I would
> rename it to nvme_mpath_alloc_ns_stat().

nvme_failover_req() would obviously be a multipath function from the 
name. Anyway, "mpath" in the name just seem better.

>> 
>>> +{
>>> +    int i, cpu;
>>> +    struct nvme_path_lat_work *work;
>>> +    gfp_t gfp = GFP_KERNEL | __GFP_ZERO;
>>> +
>>> +    if (!ns->head->disk)
>>> +        return 0;
>>> +
>>> +    ns->path_lat = __alloc_percpu_gfp(NVME_NUM_STAT_GROUPS *
>>> +                sizeof(struct nvme_path_lat),
>>> +                __alignof__(struct nvme_path_lat), gfp);
>>> +    if (!ns->path_lat)
>>> +        return -ENOMEM;
>>> +
>>> +    for_each_possible_cpu(cpu) {
>>> +        for (i = 0; i < NVME_NUM_STAT_GROUPS; i++) {
>>> +            work = &per_cpu_ptr(ns->path_lat, cpu)[i].work;
>>> +            work->ns = ns;
>>> +            work->op_type = i;
>>> +            INIT_WORK(&work->weight_work, nvme_mpath_weight_work);
>>> +        }
>>> +    }
>>> +
>>> +    return 0;
>>> +}
>>> +
>>> +static void nvme_mpath_set_ctrl_paths(struct nvme_ctrl *ctrl)
>> 
>> what do you mean by "set" here?
> 
> It is intended as the counterpart of nvme_mpath_clear_ctrl_paths().
> The former clears/disables the I/O policy state for the controller
> namespaces, while this helper sets/enables it.

To me, clear paths meaning is obvious, in that any per-NUMA node paths 
are cleared for all the paths associated with the controller.

nvme_mpath_set_ctrl_paths() does not really do the opposite - it instead 
just enables the IO latency sampling per path.

Anyway, I don't feel too strongly about this, but it just seems that the 
naming could be improved.

>> 
>>> +{
>>> +    struct nvme_ns *ns;
>>> +    int srcu_idx;
>>> +
>>> +    srcu_idx = srcu_read_lock(&ctrl->srcu);
>>> +    list_for_each_entry_srcu(ns, &ctrl->namespaces, list,
>>> +                srcu_read_lock_held(&ctrl->srcu))
>>> +        nvme_enable_ns_latency_sampling(ns);
>>> +    srcu_read_unlock(&ctrl->srcu, srcu_idx);
>>> +}
>>> +
>>>    void nvme_mpath_revalidate_paths(struct nvme_ns_head *head)
>>>    {
>>>        sector_t capacity = get_capacity(head->disk);
>>> @@ -280,6 +605,8 @@ void nvme_mpath_revalidate_paths(struct nvme_ns_head *head)
>>>                     srcu_read_lock_held(&head->srcu)) {
>>>            if (capacity != get_capacity(ns->disk))
>>>                clear_bit(NVME_NS_READY, &ns->flags);
>>> +
>>> +        nvme_reset_ns_latency_stat(ns);
>>>        }
>>>        srcu_read_unlock(&head->srcu, srcu_idx);
>>> @@ -404,6 +731,92 @@ static struct nvme_ns *nvme_round_robin_path(struct nvme_ns_head *head)
>>>        return found;
>>>    }
>>> +static inline bool nvme_state_is_live(enum nvme_ana_state state)
...

>>>        }
>>>        mutex_unlock(&head->lock);
>>> +    mutex_lock(&nvme_subsystems_lock);
>> 
>> I am curious - why use the nvme_subsystems_lock?
>> 
> nvme_subsys_iopolicy_update() and nvme_mpath_set_live() can run concurrently.
> nvme_subsystems_lock serializes these paths so that latency sampling is
> enabled consistently with the subsystem I/O policy.

ok, maybe then please consider a comment. It can be useful.


>>> @@ -527,6 +530,30 @@ enum nvme_stat_group {
>>>        NVME_NUM_STAT_GROUPS
>>>    };
>>> +struct nvme_path_lat_stat {
>>> +    u64 nr_samples;        /* total num of samples processed */
>> 
>> why u64 and not unsigned long long?
>> 
> I used u64 intentionally because this is a monotonically increasing
> sample counter, and I wanted a fixed-width 64-bit type. I didn't see
> any particular advantage in using unsigned long long here. If there's
> a reason to prefer it in this context, I'm happy to change it.

hmmm... I thought that in general we only should use a fixed width type 
when it is required, e.g. reading from a 32b register, then use u32.






More information about the Linux-nvme mailing list