s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias

Turn the middle of the ft_bias range (1..99) into an adaptive heuristic
that switches between fulltrack write (ft1) and plain write ft0 depending
on how sparse the device still is.

A sparse device benefits from fulltrack writes (it avoids the format/retry
cycle); once enough tracks are formatted the per-write overhead of
ft1 outweighs that. An state machine  measures the NRF rate in short ft0
probe windows and flips back to ft1 when it is high
(FT1_ACTIVE -> PROBING -> FT0_STABLE, with a backing-off reprobe interval).

The four parameters are derived from ft_bias by linear interpolation,
anchored so ft_bias == 50 derives the following values:

ese_heu_start_interval - 2000 - IOs in ft1, before first ft0-Probe starts
ese_heu_probe_window - 100 - IOs in probe window
ese_heu_nrf_high - 10 ‰ (= 1 %) - TRACK_FORMAT rate that leads to ft1
ese_heu_max_interval - 500000 - Backoff-Cap: max. IOs between two probes

Higher is more eager to use ft1, and 0/100 skips the heuristic.
The NRF counter is bumped in dasd_eckd_ese_format() for both the classic
NRF sense and the HPF INV_TRACK_FORMAT equivalent.
The state machine resets to ft1 on check_characteristics, full format,
and release-space.

A read-only ese_heuristic_state sysfs attribute exposes the current mode.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Link: https://patch.msgid.link/20260805111612.1285190-15-sth@linux.ibm.com
Signed-off-by: Jens Axboe <axboe@kernel.dk>
This commit is contained in:
Stefan Haberland 2026-08-05 13:16:07 +02:00 committed by Jens Axboe
parent 42849375e9
commit 4e304b2e56
3 changed files with 223 additions and 6 deletions

View File

@ -1659,8 +1659,14 @@ static ssize_t full_track_bias_store(struct device *dev,
if (IS_ERR(device))
return -ENODEV;
/*
* ft_bias is the tuning target; fulltrack is a best-effort mode hint
* that the per-IO heuristic also updates locklessly. A racing writer can
* at most leave a transient mismatch that self-corrects on the next IO,
* never corruption, so the update is left unlocked.
*/
device->ft_bias = val;
device->fulltrack = val ? 1 : 0;
dasd_ft_bias_apply(device);
dasd_put_device(device);
return count;
@ -1668,6 +1674,44 @@ static ssize_t full_track_bias_store(struct device *dev,
static DEVICE_ATTR_RW(full_track_bias);
static const char * const dasd_ese_heu_state_names[] = {
[DASD_ESE_HEU_FT1_ACTIVE] = "fulltrack active",
[DASD_ESE_HEU_PROBING] = "probing",
[DASD_ESE_HEU_FT0_STABLE] = "fulltrack inactive",
};
/* read-only: current full-track mode / adaptive FSM state, for observability */
static ssize_t
ese_heuristic_state_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct dasd_device *device;
unsigned int state;
int len;
device = dasd_device_from_cdev(to_ccwdev(dev));
if (IS_ERR(device))
return -ENODEV;
if (device->ft_bias == 0) {
len = sysfs_emit(buf, "fulltrack deactivated\n");
} else if (device->ft_bias >= DASD_FT_BIAS_MAX) {
len = sysfs_emit(buf, "fulltrack permanent active\n");
} else if (!dasd_ese_adaptive(device)) {
/* adaptive range but not ESE: the heuristic does not run */
len = sysfs_emit(buf, "fulltrack deactivated\n");
} else {
state = device->ese_probe_state;
if (state < ARRAY_SIZE(dasd_ese_heu_state_names))
len = sysfs_emit(buf, "%s\n", dasd_ese_heu_state_names[state]);
else
len = sysfs_emit(buf, "unknown\n");
}
dasd_put_device(device);
return len;
}
static DEVICE_ATTR_RO(ese_heuristic_state);
static ssize_t
dasd_retries_show(struct device *dev, struct device_attribute *attr, char *buf)
{
@ -2464,6 +2508,7 @@ static struct attribute * dasd_attrs[] = {
&dev_attr_failfast.attr,
&dev_attr_expires.attr,
&dev_attr_full_track_bias.attr,
&dev_attr_ese_heuristic_state.attr,
&dev_attr_retries.attr,
&dev_attr_timeout.attr,
&dev_attr_reservation_policy.attr,

View File

@ -2160,11 +2160,6 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
device->path_interval = DASD_ECKD_PATH_INTERVAL;
device->aq_timeouts = DASD_RETRIES_MAX;
/* default ESE fulltrack write aggressiveness from the module parameter */
device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
/* only the "always" endpoint forces fulltrack unconditionally here */
device->fulltrack = (device->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
if (private->conf.gneq) {
value = 1;
for (i = 0; i < private->conf.gneq->timeout.value; i++)
@ -2219,6 +2214,13 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
/* Read Volume Information */
dasd_eckd_read_vol_info(device);
/*
* is_ese() now reflects the hardware ESE state, so derive the default
* fulltrack write bias from the module parameter.
*/
device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
dasd_ft_bias_apply(device);
/* Read Extent Pool Information */
dasd_eckd_read_ext_pool_info(device);
@ -3171,6 +3173,13 @@ static int dasd_eckd_format_process_data(struct dasd_device *base,
static int dasd_eckd_format_device(struct dasd_device *base,
struct format_data_t *fdata, int enable_pav)
{
/*
* A full format (start_unit == 0) returns the device to a fully sparse
* state, so restart the heuristic from ft1 without an offline cycle.
*/
if (fdata->start_unit == 0)
dasd_ft_bias_apply(base);
return dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
0, NULL);
}
@ -3230,6 +3239,69 @@ static void clear_format_track(struct dasd_format_entry *format,
spin_unlock_irqrestore(&block->format_lock, flags);
}
/*
* Adaptive ft_bias heuristic, called once per IO from dasd_eckd_build_cp().
* Probes the device formatting state by briefly switching to ft0 and measuring
* the NRF rate; parameters are derived from ft_bias.
*/
static void dasd_ese_heuristic_tick(struct dasd_device *basedev)
{
int ios, nrf, rate;
if (atomic_inc_return(&basedev->ese_io_cnt) < (int)basedev->ese_probe_interval)
return;
/*
* One wins the race to evaluate, the rest see ios == 0 after the
* xchg and return early, preventing redundant state transitions.
*/
ios = atomic_xchg(&basedev->ese_io_cnt, 0);
if (ios <= 0)
return;
switch (basedev->ese_probe_state) {
case DASD_ESE_HEU_FT1_ACTIVE:
/* Start ft0 probe window, reset NRF counter for clean measurement */
basedev->fulltrack = 0;
basedev->ese_probe_state = DASD_ESE_HEU_PROBING;
basedev->ese_probe_interval = basedev->ese_heu_probe_window;
atomic_set(&basedev->ese_nrf_window, 0);
break;
case DASD_ESE_HEU_PROBING:
case DASD_ESE_HEU_FT0_STABLE:
nrf = atomic_xchg(&basedev->ese_nrf_window, 0);
rate = (int)((u64)nrf * 1000 / ios);
if (rate > (int)basedev->ese_heu_nrf_high) {
/* NRF rate high: device still sparse, ft1 is better */
basedev->fulltrack = 1;
basedev->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
basedev->ese_probe_interval = basedev->ese_heu_start_interval;
} else if (basedev->ese_probe_state == DASD_ESE_HEU_PROBING) {
/*
* NRF rate low: device mostly formatted, ft0 is faster.
* Re-probe frequently at first, then back off below.
*/
basedev->fulltrack = 0;
basedev->ese_probe_state = DASD_ESE_HEU_FT0_STABLE;
basedev->ese_probe_interval = basedev->ese_heu_probe_window;
} else {
/*
* Still stable in ft0: re-assert plain-write mode so a
* fulltrack value left behind by a racing sysfs write
* self-corrects, and back off the re-probe interval
* (double it, capped at max_interval) so a long-lived
* formatted device is not probed more often than needed.
*/
basedev->fulltrack = 0;
basedev->ese_probe_interval =
min(basedev->ese_probe_interval * 2,
basedev->ese_heu_max_interval);
}
break;
}
}
static void dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
struct irb *irb)
{
@ -3254,6 +3326,8 @@ static void dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_r
block = cqr->block;
base = block->base;
private = base->private;
if (dasd_ese_adaptive(base))
atomic_inc(&base->ese_nrf_window);
blksize = block->bp_block;
recs_per_trk = recs_per_track(&private->rdc_data, 0, blksize);
@ -4017,6 +4091,14 @@ static int dasd_eckd_release_space_full(struct dasd_device *device)
rc = dasd_sleep_on_interruptible(cqr);
if (!rc) {
/*
* Releasing all space (RAS) wipes every track and the device is fully
* sparse again, so restart the heuristic from ft1.
*/
dasd_ft_bias_apply(device);
}
dasd_sfree_request(cqr, cqr->memdev);
return rc;
@ -5185,6 +5267,11 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
struct dasd_ccw_req *cqr;
basedev = block->base;
if (dasd_ese_adaptive(basedev))
dasd_ese_heuristic_tick(basedev);
else
/* re-assert the endpoint mode: a stale heuristic write cannot stick */
basedev->fulltrack = (basedev->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
private = basedev->private;
/* Calculate number of blocks/records per track. */

View File

@ -633,6 +633,15 @@ struct dasd_device {
/* ESE fulltrack write control (see full_track_bias sysfs attribute) */
unsigned int ft_bias; /* aggressiveness 0..100: 0=off, 100=always */
unsigned int fulltrack; /* internal: use WRITE_FULL_TRACK for aligned writes */
/* adaptive heuristic (active for ft_bias 1..99), derived from ft_bias */
unsigned int ese_probe_state; /* heuristic FSM state */
unsigned int ese_probe_interval; /* IOs between evaluations */
atomic_t ese_io_cnt; /* IO counter for current window */
atomic_t ese_nrf_window; /* NRF/INV_TRACK_FORMAT events in window */
unsigned int ese_heu_start_interval; /* IOs before first probe */
unsigned int ese_heu_probe_window; /* IOs in probe window */
unsigned int ese_heu_max_interval; /* max IOs between probes (backoff cap) */
unsigned int ese_heu_nrf_high; /* NRF per-mille threshold → activate ft1 */
};
struct dasd_block {
@ -704,6 +713,25 @@ struct dasd_queue {
#define DASD_FT_BIAS_MAX 100
#define DASD_FT_BIAS_DEFAULT 50
/* ESE fulltrack heuristic FSM states (adaptive range, ft_bias 1..99) */
#define DASD_ESE_HEU_FT1_ACTIVE 0 /* fulltrack write active */
#define DASD_ESE_HEU_PROBING 1 /* ft0 probe window, measuring NRF rate */
#define DASD_ESE_HEU_FT0_STABLE 2 /* device formatted, ft0 active */
/*
* Heuristic parameters are derived from ft_bias by linear interpolation,
* anchored so that ft_bias == 50 reproduces the previously shipped defaults
* and ft_bias == 100 is the most aggressive end of the range.
* probe_window is constant.
*/
#define DASD_ESE_HEU_PROBE_WINDOW 100
#define DASD_ESE_HEU_NRF_HIGH_A50 10 /* NRF per-mille threshold */
#define DASD_ESE_HEU_NRF_HIGH_A100 1
#define DASD_ESE_HEU_START_A50 2000 /* IOs before first probe */
#define DASD_ESE_HEU_START_A100 500
#define DASD_ESE_HEU_MAX_A50 500000 /* backoff cap */
#define DASD_ESE_HEU_MAX_A100 20000
/* per device flags */
#define DASD_FLAG_OFFLINE 3 /* device is in offline processing */
#define DASD_FLAG_EER_SNSS 4 /* A SNSS is required */
@ -866,6 +894,63 @@ static inline bool dasd_req_conflict(struct dasd_ccw_req *cqr1,
cqr2->end_trk < cqr1->format->start_trk);
}
/*
* true when device is ese device and ft_bias selects the adaptive
* heuristic (neither hard endpoint)
*/
static inline bool dasd_ese_adaptive(struct dasd_device *device)
{
return device->discipline &&
device->discipline->is_ese &&
device->discipline->is_ese(device) &&
device->ft_bias > 0 &&
device->ft_bias < DASD_FT_BIAS_MAX;
}
/*
* Linear interpolation of a heuristic parameter between its value at aggr==50
* (v50) and its value at aggr==100 (v100).
*/
static inline unsigned int dasd_ese_lerp(unsigned int v50, unsigned int v100,
unsigned int aggr)
{
return (unsigned int)((int)v50 +
((int)v100 - (int)v50) * ((int)aggr - 50) / 50);
}
/*
* Apply the ft_bias knob. For the hard endpoints just pin the mode; for the
* adaptive range derive the heuristic parameters from ft_bias and (re)start
* the FSM in ft1 so a freshly sparse device avoids the NRF penalty right away.
*/
static inline void dasd_ft_bias_apply(struct dasd_device *device)
{
unsigned int a = device->ft_bias;
if (!dasd_ese_adaptive(device)) {
device->fulltrack = (a >= DASD_FT_BIAS_MAX) ? 1 : 0;
device->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
return;
}
device->ese_heu_nrf_high =
dasd_ese_lerp(DASD_ESE_HEU_NRF_HIGH_A50,
DASD_ESE_HEU_NRF_HIGH_A100, a);
device->ese_heu_start_interval =
dasd_ese_lerp(DASD_ESE_HEU_START_A50,
DASD_ESE_HEU_START_A100, a);
device->ese_heu_max_interval =
dasd_ese_lerp(DASD_ESE_HEU_MAX_A50,
DASD_ESE_HEU_MAX_A100, a);
device->ese_heu_probe_window = DASD_ESE_HEU_PROBE_WINDOW;
device->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
device->ese_probe_interval = device->ese_heu_start_interval;
device->fulltrack = 1;
atomic_set(&device->ese_io_cnt, 0);
atomic_set(&device->ese_nrf_window, 0);
}
/* externals in dasd.c */
#define DASD_PROFILE_OFF 0
#define DASD_PROFILE_ON 1