Linux Kernel Selftest development
 help / color / mirror / Atom feed
From: Tao Cui <cui.tao@linux.dev>
To: Alexei Starovoitov <alexei.starovoitov@gmail.com>,
	tj@kernel.org, josef@toxicopanda.com, axboe@kernel.dk
Cc: cui.tao@linux.dev, cgroups@vger.kernel.org,
	linux-block@vger.kernel.org, linux-kernel@vger.kernel.org,
	bpf@vger.kernel.org, andrii@kernel.org, eddyz87@gmail.com,
	daniel@iogearbox.net, linux-kselftest@vger.kernel.org,
	cuitao@kylinos.cn, ameryhung@gmail.com
Subject: Re: [RFC PATCH v11 0/5] blk-iocost: BPF struct_ops cost model
Date: Thu, 29 Oct 2026 17:55:13 +0800	[thread overview]
Message-ID: <610ffd7a-fa8f-465f-99d6-04be1dab02c2@linux.dev> (raw)
In-Reply-To: <DMGLGKYOLXFA.12EJEMVN58Y5P@gmail.com>

Hello Alexei,

在 2026/10/28 23:58, Alexei Starovoitov 写道:
> On Wed, Oct 28, 2026 at 07:35 AM Tao Cui <cui.tao@linux.dev> wrote:
>> The cgroup callbacks are bound to the iocg policy lifetime and pair
>> up exactly: iocg_init() is delivered from ioc_pd_online(), which
>> blkg_create() only calls after the blkg has been inserted into the
>> radix tree and the queue's blkg list, and on attach to every cgroup
>> which is already online on the device; iocg_free() is delivered at
>> detach to the cgroups which still exist then and at pd_free() time
>> for the rest, gated per iocg on a delivered flag.  The attach and
>> detach walks go over q->blkg_list under q->blkcg_mutex and
>> q->queue_lock, the same locking blkg creation and destruction use.
> 
> Who needs iocg_init() and iocg_free()?
> Both example models keep their state in BPF_MAP_TYPE_CGRP_STORAGE.
> The only user of the callbacks is a pair of counters in the selftest.
> 
Agreed. There isn't a model planned that needs them.
> One struct_ops map is bound to one device. So the cgroup storage of
> a model is per (cgroup, device) already.
> The blkg holds the blkcg css until blkg_free_workfn(). The storage is
> freed with the cgroup or with the map. So it outlives the iocg.
> 
Understood. That's already how the example models work.
> The v11 changelog says that calc_cost() can run before iocg_init().
> So a model has to create its state in calc_cost() anyway.
> iocg_init() runs under queue_lock and ioc->lock. An allocation there
> is not better than in calc_cost().
> 
Right. There isn't anything the callbacks can do that calc_cost() can't.
> v8, v9, v10 and v11 all reworked the pairing of these two callbacks.
> 
> Lets drop them. A struct_ops can get new members later, when a model
> needs them.
Dropped in v12. If a model needs them later, struct_ops can grow new
members then.

> I'd like to hear from Tejun whether such a model is planned.
> 
> Without the callbacks there is no walk of q->blkg_list:
> 
>   what                v11                          without callbacks
>   ------------------  ---------------------------  ------------------
>   attach takes        freeze, rq_qos_mutex,        freeze,
>                       quiesce, blkcg_mutex,        rq_qos_mutex,
>                       queue_lock, ioc->lock        ioc->lock
>   detach takes        rq_qos_mutex, queue_lock,    rq_qos_mutex,
>                       ioc->lock to check, then     ioc->lock
>                       freeze and the same again,
>                       quiesce, blkcg_mutex,
>                       queue_lock, ioc->lock
>   state per iocg      bpf_iocg_inited              none
>   hooks in            ioc_pd_online(),             none
>                       ioc_pd_free()
>   teardown            ioc_bpf_detach(),            ioc_detach_model()
>                       ioc_bpf_eject()
> 
> calc_cost() runs under rcu_read_lock(). bpf_map_put() frees a struct_ops
> map after an RCU gp. So detach doesn't need to freeze the queue.
> The rest fits into blk-iocost.c and patch 1 is not needed.
> 
Done. The detach freeze is gone in v12, everything is folded back into
blk-iocost.c, and patch 1 is dropped.

> The diff keeps the map reference, the owning link and the
> model=linear/bpf switch. It has no docs, no selftests and no tracepoint.
> 
> Something like this (compile tested only):
> 
> diff --git a/block/Kconfig b/block/Kconfig
> index 70e4a66..26badf5 100644
> --- a/block/Kconfig
> +++ b/block/Kconfig
> @@ -161,6 +161,10 @@ config BLK_CGROUP_IOCOST
>  	distributes IO capacity between different groups based on
>  	their share of the overall weight distribution.
> 
> +config BLK_CGROUP_IOCOST_BPF
> +	def_bool y
> +	depends on BLK_CGROUP_IOCOST && BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
> +
>  config BLK_CGROUP_IOPRIO
>  	bool "Cgroup I/O controller for assigning an I/O priority class"
>  	depends on BLK_CGROUP
> diff --git a/block/blk-iocost.c b/block/blk-iocost.c
> index a81488c..5e65bf4 100644
> --- a/block/blk-iocost.c
> +++ b/block/blk-iocost.c
> @@ -178,8 +178,12 @@
>  #include <linux/time64.h>
>  #include <linux/parser.h>
>  #include <linux/sched/signal.h>
> +#include <linux/bpf.h>
> +#include <linux/bpf_verifier.h>
> +#include <linux/btf.h>
>  #include <asm/local.h>
>  #include <asm/local64.h>
> +#include "blk.h"
>  #include "blk-rq-qos.h"
>  #include "blk-stat.h"
>  #include "blk-wbt.h"
> @@ -404,6 +408,22 @@ struct ioc_pcpu_stat {
>  	u64				last_rq_wait_ns;
>  };
> 
> +enum {
> +	IOCOST_COST_F_MERGE	= 1 << 0,	/* called from the merge path */
> +};
> +
> +/* bpf cost model of one device */
> +struct iocost_model_ops {
> +	/* whole disk, in the encoding of new_decode_dev() */
> +	dev_t dev;
> +
> +	/* used by the completion path instead of LCOEF_[RW]PAGE */
> +	u64 read_vtime_per_page;
> +	u64 write_vtime_per_page;
> +
> +	u64 (*calc_cost)(struct bio *bio, u64 model_flags);
> +};
> +
>  /* per device */
>  struct ioc {
>  	struct rq_qos			rqos;
> @@ -447,6 +467,12 @@ struct ioc {
>  	int				autop_idx;
>  	bool				user_qos_params:1;
>  	bool				user_cost_model:1;
> +
> +	/* holds a reference on the struct_ops map, NULL if none is attached */
> +	const struct iocost_model_ops	*attached;
> +	struct bpf_link			*attached_link;
> +	/* either NULL (linear) or ->attached */
> +	const struct iocost_model_ops __rcu *model;
>  };
> 
>  struct iocg_pcpu_stat {
> @@ -669,6 +695,13 @@ static struct ioc *q_to_ioc(struct request_queue *q)
>  	return rqos_to_ioc(rq_qos_id(q, RQ_QOS_COST));
>  }
> 
> +static const struct iocost_model_ops *ioc_model(struct ioc *ioc)
> +{
> +	if (!IS_ENABLED(CONFIG_BLK_CGROUP_IOCOST_BPF))
> +		return NULL;
> +	return rcu_dereference_check(ioc->model, lockdep_is_held(&ioc->lock));
> +}
> +
>  static const char __maybe_unused *ioc_name(struct ioc *ioc)
>  {
>  	struct gendisk *disk = ioc->rqos.disk;
> @@ -806,7 +839,7 @@ static int ioc_autop_idx(struct ioc *ioc, struct gendisk *disk)
>  		return AUTOP_SSD_DFL;
> 
>  	/* if user is overriding anything, maintain what was there */
> -	if (ioc->user_qos_params || ioc->user_cost_model)
> +	if (ioc->user_qos_params || ioc->user_cost_model || ioc_model(ioc))
>  		return idx;
> 
>  	/* step up/down based on the vrate */
> @@ -2586,8 +2619,16 @@ out:
> 
>  static u64 calc_vtime_cost(struct bio *bio, struct ioc_gq *iocg, bool is_merge)
>  {
> +	const struct iocost_model_ops *model;
>  	u64 cost;
> 
> +	guard(rcu)();
> +	model = ioc_model(iocg->ioc);
> +	if (model) {
> +		cost = model->calc_cost(bio, is_merge ? IOCOST_COST_F_MERGE : 0);
> +		return min(cost, VTIME_PER_SEC);
> +	}
> +
>  	calc_vtime_cost_builtin(bio, iocg, is_merge, &cost);
>  	return cost;
>  }
> @@ -2612,8 +2653,16 @@ static void calc_size_vtime_cost_builtin(struct request *rq, struct ioc *ioc,
> 
>  static u64 calc_size_vtime_cost(struct request *rq, struct ioc *ioc)
>  {
> +	const struct iocost_model_ops *model;
>  	u64 cost;
> 
> +	guard(rcu)();
> +	model = ioc_model(ioc);
> +	if (model)
> +		return (blk_rq_stats_sectors(rq) >> IOC_SECT_TO_PAGE_SHIFT) *
> +			(req_op(rq) == REQ_OP_READ ? model->read_vtime_per_page :
> +						     model->write_vtime_per_page);
> +
>  	calc_size_vtime_cost_builtin(rq, ioc, &cost);
>  	return cost;
>  }
> @@ -2908,10 +2957,33 @@ static void ioc_rqos_queue_depth_changed(struct rq_qos *rqos)
>  	spin_unlock_irq(&ioc->lock);
>  }
> 
> +/* Called by .unreg and by device removal, whichever is first. */
> +static void ioc_detach_model(struct ioc *ioc)
> +{
> +#ifdef CONFIG_BLK_CGROUP_IOCOST_BPF
> +	const struct iocost_model_ops *ops = ioc->attached;
> +
> +	lockdep_assert_held(&ioc->rqos.disk->queue->rq_qos_mutex);
> +
> +	if (!ops)
> +		return;
> +
> +	spin_lock_irq(&ioc->lock);
> +	rcu_assign_pointer(ioc->model, NULL);
> +	ioc->attached = NULL;
> +	ioc->attached_link = NULL;
> +	spin_unlock_irq(&ioc->lock);
> +
> +	/* calc_cost() can still run. The map is freed after an RCU gp. */
> +	bpf_struct_ops_put(ops);
> +#endif
> +}
> +
>  static void ioc_rqos_exit(struct rq_qos *rqos)
>  {
>  	struct ioc *ioc = rqos_to_ioc(rqos);
> 
> +	ioc_detach_model(ioc);
>  	blkcg_deactivate_policy(rqos->disk, &blkcg_policy_iocost);
> 
>  	spin_lock_irq(&ioc->lock);
> @@ -3458,11 +3530,12 @@ static u64 ioc_cost_model_prfill(struct seq_file *sf,
>  		return 0;
> 
>  	spin_lock_irq(&ioc->lock);
> -	seq_printf(sf, "%s ctrl=%s model=linear "
> +	seq_printf(sf, "%s ctrl=%s model=%s "
>  		   "rbps=%llu rseqiops=%llu rrandiops=%llu "
>  		   "wbps=%llu wseqiops=%llu wrandiops=%llu "
>  		   "flushiops=%llu\n",
>  		   dname, ioc->user_cost_model ? "user" : "auto",
> +		   ioc_model(ioc) ? "bpf" : "linear",
>  		   u[I_LCOEF_RBPS], u[I_LCOEF_RSEQIOPS], u[I_LCOEF_RRANDIOPS],
>  		   u[I_LCOEF_WBPS], u[I_LCOEF_WSEQIOPS], u[I_LCOEF_WRANDIOPS],
>  		   u[I_LCOEF_FLUSHIOPS]);
> @@ -3499,6 +3572,7 @@ static const match_table_t i_lcoef_tokens = {
>  static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
>  				    size_t nbytes, loff_t off)
>  {
> +	const struct iocost_model_ops *model;
>  	struct blkg_conf_ctx ctx;
>  	struct request_queue *q;
>  	unsigned int memflags;
> @@ -3535,6 +3609,7 @@ static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
>  	spin_lock_irq(&ioc->lock);
>  	memcpy(u, ioc->params.i_lcoefs, sizeof(u));
>  	user = ioc->user_cost_model;
> +	model = ioc_model(ioc);
> 
>  	ret = -EINVAL;
> 
> @@ -3559,7 +3634,11 @@ static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
>  			continue;
>  		case COST_MODEL:
>  			match_strlcpy(buf, &args[0], sizeof(buf));
> -			if (strcmp(buf, "linear"))
> +			if (!strcmp(buf, "linear"))
> +				model = NULL;
> +			else if (!strcmp(buf, "bpf") && ioc->attached)
> +				model = ioc->attached;
> +			else
>  				goto unlock;
>  			continue;
>  		}
> @@ -3579,6 +3658,7 @@ static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
>  	} else {
>  		ioc->user_cost_model = false;
>  	}
> +	rcu_assign_pointer(ioc->model, model);
>  	ioc_refresh_params(ioc, true);
> 
>  	ret = 0;
> @@ -3626,6 +3706,158 @@ static struct blkcg_policy blkcg_policy_iocost = {
>  	.pd_stat_fn	= ioc_pd_stat,
>  };
> 
> +#ifdef CONFIG_BLK_CGROUP_IOCOST_BPF
> +static int ioc_bpf_reg(void *kdata, struct bpf_link *link)
> +{
> +	struct iocost_model_ops *ops = kdata;
> +	struct block_device *bdev;
> +	struct request_queue *q;
> +	unsigned int memflags;
> +	struct ioc *ioc;
> +	int ret;
> +
> +	bdev = blkdev_get_no_open(new_decode_dev(ops->dev), false);
> +	if (!bdev)
> +		return -ENODEV;
> +
> +	q = bdev_get_queue(bdev);
> +	if (bdev_is_partition(bdev) || !queue_is_mq(q)) {
> +		blkdev_put_no_open(bdev);
> +		return -EOPNOTSUPP;
> +	}
> +
> +	/* the order of ioc_qos_write() */
> +	memflags = blk_mq_freeze_queue(q);
> +	mutex_lock(&q->rq_qos_mutex);
> +
> +	ret = -ENODEV;
> +	if (!disk_live(bdev->bd_disk))
> +		goto out;
> +
> +	ioc = q_to_ioc(q);
> +	if (!ioc) {
> +		ret = blk_iocost_init(bdev->bd_disk);
> +		if (ret)
> +			goto out;
> +		ioc = q_to_ioc(q);
> +	}
> +
> +	ret = -EBUSY;
> +	if (ioc->attached || !bpf_struct_ops_get(ops))
> +		goto out;
> +
> +	spin_lock_irq(&ioc->lock);
> +	ioc->attached = ops;
> +	ioc->attached_link = link;
> +	rcu_assign_pointer(ioc->model, ops);
> +	spin_unlock_irq(&ioc->lock);
> +	ret = 0;
> +out:
> +	mutex_unlock(&q->rq_qos_mutex);
> +	blk_mq_unfreeze_queue(q, memflags);
> +	blkdev_put_no_open(bdev);
> +	return ret;
> +}
> +
> +static void ioc_bpf_unreg(void *kdata, struct bpf_link *link)
> +{
> +	struct iocost_model_ops *ops = kdata;
> +	struct block_device *bdev;
> +	struct request_queue *q;
> +	struct ioc *ioc;
> +
> +	/* del_gendisk() is in progress or done. ioc_rqos_exit() detaches. */
> +	bdev = blkdev_get_no_open(new_decode_dev(ops->dev), false);
> +	if (!bdev)
> +		return;
> +
> +	q = bdev_get_queue(bdev);
> +	mutex_lock(&q->rq_qos_mutex);
> +	ioc = q_to_ioc(q);
> +	/* the dev_t can belong to another disk and another model by now */
> +	if (ioc && ioc->attached == ops && ioc->attached_link == link)
> +		ioc_detach_model(ioc);
> +	mutex_unlock(&q->rq_qos_mutex);
> +	blkdev_put_no_open(bdev);
> +}
> +
> +static int ioc_bpf_init(struct btf *btf)
> +{
> +	return 0;
> +}
> +
> +static int ioc_bpf_check_member(const struct btf_type *t,
> +				const struct btf_member *member,
> +				const struct bpf_prog *prog)
> +{
> +	return prog->sleepable ? -EINVAL : 0;
> +}
> +
> +static int ioc_bpf_init_member(const struct btf_type *t,
> +			       const struct btf_member *member,
> +			       void *kdata, const void *udata)
> +{
> +	const struct iocost_model_ops *uops = udata;
> +	struct iocost_model_ops *ops = kdata;
> +
> +	switch (__btf_member_bit_offset(t, member) / 8) {
> +	case offsetof(struct iocost_model_ops, dev):
> +		ops->dev = uops->dev;
> +		return 1;
> +	case offsetof(struct iocost_model_ops, read_vtime_per_page):
> +		ops->read_vtime_per_page = uops->read_vtime_per_page;
> +		return 1;
> +	case offsetof(struct iocost_model_ops, write_vtime_per_page):
> +		ops->write_vtime_per_page = uops->write_vtime_per_page;
> +		return 1;
> +	}
> +	return 0;
> +}
> +
> +static int ioc_bpf_validate(void *kdata)
> +{
> +	struct iocost_model_ops *ops = kdata;
> +
> +	if (!ops->dev || !ops->calc_cost ||
> +	    !ops->read_vtime_per_page || !ops->write_vtime_per_page)
> +		return -EINVAL;
> +	return 0;
> +}
> +
> +static u64 ioc_bpf_calc_cost_stub(struct bio *bio, u64 model_flags)
> +{
> +	return 0;
> +}
> +
> +static struct iocost_model_ops ioc_bpf_stubs = {
> +	.calc_cost	= ioc_bpf_calc_cost_stub,
> +};
> +
> +static const struct bpf_verifier_ops ioc_bpf_verifier_ops = {
> +	.get_func_proto		= bpf_base_func_proto,
> +	.is_valid_access	= bpf_tracing_btf_ctx_access,
> +};
> +
> +static struct bpf_struct_ops ioc_bpf_struct_ops = {
> +	.verifier_ops	= &ioc_bpf_verifier_ops,
> +	.init		= ioc_bpf_init,
> +	.check_member	= ioc_bpf_check_member,
> +	.init_member	= ioc_bpf_init_member,
> +	.validate	= ioc_bpf_validate,
> +	.reg		= ioc_bpf_reg,
> +	.unreg		= ioc_bpf_unreg,
> +	.cfi_stubs	= &ioc_bpf_stubs,
> +	.name		= "iocost_model_ops",
> +	.owner		= THIS_MODULE,
> +};
> +
> +static int __init ioc_bpf_struct_ops_init(void)
> +{
> +	return register_bpf_struct_ops(&ioc_bpf_struct_ops, iocost_model_ops);
> +}
> +late_initcall(ioc_bpf_struct_ops_init);
> +#endif
> +
>  static int __init ioc_init(void)
>  {
>  	return blkcg_policy_register(&blkcg_policy_iocost);
> 
>>  3/5: selftests with two example models (the full builtin linear
>>       HDD formula at double cost, and a multi-stream sequentiality
>>       model) plus a runner and the selftest kernel config entries
>>  4/5: add an iocost_ioc_tick tracepoint emitting the per-period
>>       controller state, so model quality can be evaluated without
>>       drgn (existing events are state-change driven and silent in
>>       steady state)
> 
> Also progs/iocost_model.c in patch 3 has SEC("tp_btf/iocost_ioc_tick").
> Patch 4 adds this tracepoint. At patch 3 iocost_model__load() fails.
> Swap the two patches.
> 
Swapped: the iocost_ioc_tick tracepoint is patch 2 now and the
selftest is patch 3, so iocost_model__load() works at every commit.
The selftest also drives IO through iocost_ms's calc_cost().

Thanks,
Tao

> pw-bot: cr


  parent reply	other threads:[~2026-10-29  9:55 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28 14:35 [RFC PATCH v11 0/5] blk-iocost: BPF struct_ops cost model Tao Cui
2026-10-28 14:35 ` [PATCH 1/5] blk-iocost: move shared definitions to blk-iocost.h Tao Cui
2026-10-28 20:09   ` Tejun Heo
2026-10-29 10:04     ` Tao Cui
2026-10-28 14:35 ` [PATCH 2/5] blk-iocost: add BPF struct_ops cost model support Tao Cui
2026-10-28 15:33   ` bot+bpf-ci
2026-10-28 20:09   ` Tejun Heo
2026-10-29 10:11     ` Tao Cui
2026-10-28 14:35 ` [PATCH 3/5] selftests/bpf: add iocost cost model test Tao Cui
2026-10-28 15:33   ` bot+bpf-ci
2026-10-28 20:09   ` Tejun Heo
2026-10-29 10:30     ` Tao Cui
2026-10-28 14:35 ` [PATCH 4/5] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
2026-10-28 20:09   ` Tejun Heo
2026-10-29 10:31     ` Tao Cui
2026-10-28 14:35 ` [PATCH 5/5] docs: cgroup-v2: document the iocost BPF cost model attachment Tao Cui
2026-10-28 20:09   ` Tejun Heo
2026-10-29 10:33     ` Tao Cui
2026-10-28 15:58 ` [RFC PATCH v11 0/5] blk-iocost: BPF struct_ops cost model Alexei Starovoitov
2026-10-28 20:09   ` Tejun Heo
2026-10-29  9:55   ` Tao Cui [this message]
2026-10-28 20:09 ` Tejun Heo
2026-10-29  9:57   ` Tao Cui

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=610ffd7a-fa8f-465f-99d6-04be1dab02c2@linux.dev \
    --to=cui.tao@linux.dev \
    --cc=alexei.starovoitov@gmail.com \
    --cc=ameryhung@gmail.com \
    --cc=andrii@kernel.org \
    --cc=axboe@kernel.dk \
    --cc=bpf@vger.kernel.org \
    --cc=cgroups@vger.kernel.org \
    --cc=cuitao@kylinos.cn \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=josef@toxicopanda.com \
    --cc=linux-block@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=tj@kernel.org \
    /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