mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH v7 0/4] blk-iocost: BPF struct_ops cost model
@ 2026-09-24  5:45 Tao Cui
  2026-09-24  5:45 ` [RFC PATCH v7 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
                   ` (3 more replies)
  0 siblings, 4 replies; 8+ messages in thread
From: Tao Cui @ 2026-09-24  5:45 UTC (permalink / raw)
  To: tj, josef, axboe
  Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
	daniel, linux-kselftest, cui.tao, cuitao, ameryhung,
	alexei.starovoitov

This is v7 of the RFC.  The attachment model is reworked: the
struct_ops instance is now per-device, following the hid_bpf_ops
pattern.  The name registry, the binding through io.cost.model
writes, the lifecycle list and the refcounting are gone.

Why a pluggable model at all
----------------------------

When iocost landed in 2019, its commit message already promised that
"a later patch will also allow using bpf progs for cost models", and
the code has carried the split for it ever since: calc_vtime_cost()
is a dispatcher whose only implementation is calc_vtime_cost_builtin().
Seven years later the builtin linear model is still the only one.
This series fills that slot: builtin algorithms remain the default
while new ones can be prototyped in BPF.

The measured problems
---------------------

The builtin model prices each IO with a binary sequential/random base
picked by a single per-cgroup cursor and a 16MB seek threshold, plus
a per-page cost.  On a virtio-blk device with the HDD autop profile,
a 4k IO costs ~24us when judged sequential and ~2.7ms when judged
random, a 112x spread, so a wrong judgement becomes a wrong price.
Three classes of mispricing, all measured:

 1. Heuristic rigidity.  Two legitimate sequential readers in one
    cgroup (a database with multiple tablespaces, a threaded backup)
    ping-pong the single cursor and are all priced random: a
    measured 89x overcharge collapses throughput under the same
    weight.  Random IO within a hot window smaller than the 16MB
    threshold is priced sequential: measured 107x undercharge, an
    accounting escape for hotspot workloads.  No setting of the six
    builtin parameters seems able to fix this: telling the streams
    apart requires per-IO state tracking, which looks like logic
    rather than coefficients.

 2. Device nonlinearity.  SLC-cache phases, SMR band placement and
    shared controllers (multiple NVMe namespaces multiplexing one
    device) make the real cost of an identical IO vary by an order
    of magnitude over time or across namespaces.  A static
    6-parameter linear model has no way to express that.

 3. Unpriced operations.  Flush and zone append fall through to a
    cost of zero and bypass throttling entirely, and the same pattern
    extends to device quirks the builtin model was never taught.

Mispricing feeds directly into the control loop: vtime budgets,
surplus donation and the vrate feedback all consume the model's
output, so a wrong model can skew the whole controller.

How
---

Attachment follows the hid_bpf_ops model: the target device is set
in the dev member of the struct_ops from userspace before load, as
a major:minor in the usual userspace encoding (decoded with
new_decode_dev() on the kernel side); loading attaches the model to
that device and switches it away from the builtin linear model,
detaching the struct_ops restores the builtin model, and the
struct_ops core owns the program lifetime.  There is no registry
and no bound-state bookkeeping; attaching a second model to a device
with one already attached fails with -EBUSY.

A bound model fully owns pricing for every IO on the device: it is
called from the bio charging path and prices every operation
including flushes.  The completion-time request sizing which feeds
the latency met/missed accounting uses the transfer cost
coefficients the struct_ops carries (vtime per page for reads and
writes), so the builtin latency tracking and vrate adjustment
follow the model's pricing while it is attached; extending the
model to the QoS side itself is left for a later interface.  The
builtin cursor is not exposed; a model is expected to track its own
stream state.  Model state keyed by the blkcg alone is shared
across every device the model is attached to, unlike the builtin
cursor which is per (cgroup, device); iocg_init()/iocg_free()
callbacks, invoked from the iocg policy init and free paths with
the same per (cgroup, device) lifetime, let models manage their own
per-cgroup state.

While a model is attached, "model" reads back "bpf" and "ctrl"
keeps describing the builtin coefficients, which are inert then:
coefficient writes are stored and take effect again after detach,
and the automatic profile stepping does not switch profiles.
Writing "ctrl=bpf" or "model=bpf" is accepted as a no-op so a saved
configuration still parses; re-attaching the model requires loading
the struct_ops again, not writing to this file.

    u64 calc_cost(struct bio *bio, u64 model_flags)

The model reads whatever it needs from the bio itself: the
operation flags (the operation must be extracted with a mask, and
the REQ_* flag bits, including PREFLUSH/FUA, are part of it), the
size, the start sector and the issuing cgroup through
bio->bi_blkg.  model_flags carries iocost-specific metadata which
is not a property of the bio, such as whether the cost calculation
is for a merged request; the return value is vtime, clamped to 1
second of device time per IO.  Per-cgroup state can be stored in
BPF_MAP_TYPE_CGRP_STORAGE keyed by the cgroup of bi_blkg; it
follows the cgroup lifetime.  Sleepable models are rejected at
verification, since calc_cost() runs under RCU read lock.

Patch overview:

 1/4: the BPF struct_ops cost model support: Kconfig, ops
      definition, per-device attachment, unified dispatch and
      verifier checks
 2/4: 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
 3/4: 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)
 4/4: document the attachment in cgroup-v2.rst

Does it work
------------

Mechanism, verified functionally on a virtio-blk device (HDD
profile, sequential-read workload from a 1%-weight cgroup, builtin
vs the 2x example model):

 - per-IO charge: 2882us -> 5722us, a factor of 1.985x; the
   completed IO count halves and total cost.usage is conserved,
   i.e. the model output drives both charging and budgeting
 - attachment: model=bpf readback while attached (ctrl keeps
   describing the inert builtin coefficients), EBUSY for a second
   model on the same device, a second device attaches an independent
   model, detaching restores the builtin readback
 - edge cases: binding an unknown device fails and nothing is
   applied; the selftest runner checks the write error and errno
   of every step, including the restoration

Workload-shape verification (same setup, 4k IOs at weight 1000,
builtin vs the 2x example model):

 - flush-heavy workload (read/write/fsync alternating): priced
   1.99x the builtin, i.e. flushes no longer reset the cursor
   and misjudge the following IO as random
 - non-page-multiple IO (6 KiB): priced ~2x, matching the
   builtin's truncating page count
 - first IO from a high LBA (past 16 MiB): priced 2.01x, i.e.
   a fresh cgroup's zero cursor no longer misjudges the first IO
   as random

Payoff, demonstrated with the multi-stream example model (2/4) on
the same setup, 4k IOs at weight 1000, builtin vs the model:

 - two sequential readers in one cgroup: priced 1961us/op by builtin
   (both judged random by the single cursor) and 23us/op by the
   model (each stream keeps its own slot); the completed IO count
   rises by two orders of magnitude
 - random IO inside an 8M window: priced 24us/op by builtin
   (undercharge, an accounting escape) and 2607us/op by the model
 - single-stream sequential and whole-disk random pricing are
   unchanged, so the model fixes both directions of mispricing
   without introducing a new one

Non-interference, measured on enterprise NVMe: no measurable
overhead when the BPF model is not attached.

Changes in v7:
- the attachment model is reworked along the lines of the v1 review
  feedback: the struct_ops instance is per-device following
  hid_bpf_ops, with the device in the dev member set from
  userspace before load, binding in .reg, unbinding in .unreg and
  the struct_ops core owning the lifetime; the name registry, the
  model-name handling in io.cost.model writes, the lifecycle list
  and the refcounting are removed
- calc_cost() receives the bio itself and reads the operation
  flags, size, sector and issuing cgroup from it; only the merge
  indicator stays in the separate flags argument
- the cgroup callbacks are bound to the iocg policy init/free
  paths instead of the blkcg css, giving the model the builtin
  cursor's per (cgroup, device) lifetime and dropping the global
  mutex from the cgroup online/offline paths
- the struct_ops carries the transfer cost coefficients (vtime per
  page for reads and writes), used by the completion-time request
  sizing so the builtin latency tracking and vrate adjustment
  follow the model's pricing while it is attached
- the tick tracepoint is emitted at the end of the period it
  reports, before the refresh, and the temporary snapshots are
  dropped
- the two selftest patches are combined

Link: https://lore.kernel.org/r/20260908100143.47598-1-cui.tao@linux.dev # v1
Link: https://lore.kernel.org/r/20260910125817.223354-1-cui.tao@linux.dev # v2
Link: https://lore.kernel.org/r/20260914073356.791518-1-cui.tao@linux.dev # v3
Link: https://lore.kernel.org/r/20260916072302.1068871-1-cui.tao@linux.dev # v4
Link: https://lore.kernel.org/r/20260918031751.1255420-1-cui.tao@linux.dev # v5
Link: https://lore.kernel.org/r/20260918055001.1273840-1-cui.tao@linux.dev # v6

Tao Cui (4):
  blk-iocost: add BPF struct_ops cost model support
  selftests/bpf: add iocost cost model test
  blk-iocost: add iocost_ioc_tick tracepoint for per-period device
    summary
  docs: cgroup-v2: document the iocost BPF cost model attachment

 Documentation/admin-guide/cgroup-v2.rst       |  17 ++
 block/Kconfig                                 |  10 +
 block/Makefile                                |   1 +
 block/blk-iocost-bpf.c                        | 165 ++++++++++
 block/blk-iocost.c                            | 283 +++++++++++++++++-
 include/linux/blk-iocost.h                    |  96 ++++++
 include/trace/events/iocost.h                 |  45 +++
 tools/testing/selftests/bpf/config            |   2 +
 .../selftests/bpf/prog_tests/iocost_model.c   | 180 +++++++++++
 .../selftests/bpf/progs/iocost_model.c        | 138 +++++++++
 tools/testing/selftests/bpf/progs/iocost_ms.c | 159 ++++++++++
 11 files changed, 1092 insertions(+), 4 deletions(-)
 create mode 100644 block/blk-iocost-bpf.c
 create mode 100644 include/linux/blk-iocost.h
 create mode 100644 tools/testing/selftests/bpf/prog_tests/iocost_model.c
 create mode 100644 tools/testing/selftests/bpf/progs/iocost_model.c
 create mode 100644 tools/testing/selftests/bpf/progs/iocost_ms.c

-- 
2.43.0


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

end of thread, other threads:[~2026-09-24  6:30 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-24  5:45 [RFC PATCH v7 0/4] blk-iocost: BPF struct_ops cost model Tao Cui
2026-09-24  5:45 ` [RFC PATCH v7 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
2026-09-24  6:30   ` bot+bpf-ci
2026-09-24  5:45 ` [RFC PATCH v7 2/4] selftests/bpf: add iocost cost model test Tao Cui
2026-09-24  6:30   ` bot+bpf-ci
2026-09-24  5:45 ` [RFC PATCH v7 3/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
2026-09-24  6:17   ` bot+bpf-ci
2026-09-24  5:45 ` [RFC PATCH v7 4/4] docs: cgroup-v2: document the iocost BPF cost model attachment Tao Cui

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®