From 66f4321178f1653b45cd33ff582adec32937742f Mon Sep 17 00:00:00 2001 From: wnsah814 Date: Wed, 16 Sep 2026 13:33:45 +0900 Subject: [PATCH 1/3] femu/zns: size the write caches from a zns_num_wc property A write cache stages the partial stripe of one zone, so the device needs one per zone the host may be writing at the same time. The count was hard-wired to 3: with more concurrent writers than that every write misses, evicts another zone's cache and pays that zone's flush synchronously, and the sequential-write throughput of the emulated device collapses (8 channels x 4 chips x 2 planes, TLC, 4 KiB writes: 89K IOPS with 2 writers, 6K with 4). Real ZNS SSDs bound these per-zone resources with the open-zone limit (Bjorling et al., ATC'21, sec. 3.1), and a WD ZN540 (max open 14) keeps its throughput flat up to 14 concurrent zones. Take the number of caches from a zns_num_wc property; when it is left at 0 follow zns_max_open, and keep the previous 3 when that is unlimited too. --- README.md | 2 ++ hw/femu/femu.c | 1 + hw/femu/nvme.h | 1 + hw/femu/zns/zns.c | 15 ++++++++++++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d3ea9c168e3..27e1df8c4d7 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,8 @@ Emulates NVMe ZNS SSDs with zone-based interface. ```bash zns_max_active=0 # Max active zones (0 = unlimited) zns_max_open=0 # Max open zones (0 = unlimited) +zns_num_wc=0 # Write caches, one per zone being written + # (0 = zns_max_open, or 3 when that is unlimited) zns_zd_ext_size=0 # Zone-descriptor extension bytes (0 = none) zns_num_conv_zones=0 # Leading conventional zones (0 = all sequential) zns_zone_cap=0 # Usable bytes per zone (0 = the whole zone) diff --git a/hw/femu/femu.c b/hw/femu/femu.c index edf4ca2666b..b5b5a39dc3b 100644 --- a/hw/femu/femu.c +++ b/hw/femu/femu.c @@ -1907,6 +1907,7 @@ static const Property femu_props[] = { DEFINE_PROP_INT64("zns_status_lat", FemuCtrl, zns_params.zns_status_lat, 0), DEFINE_PROP_UINT32("zns_max_active", FemuCtrl, zns_params.zns_max_active, 0), DEFINE_PROP_UINT32("zns_max_open", FemuCtrl, zns_params.zns_max_open, 0), + DEFINE_PROP_UINT32("zns_num_wc", FemuCtrl, zns_params.zns_num_wc, 0), DEFINE_PROP_UINT32("zns_zd_ext_size", FemuCtrl, zns_params.zns_zd_ext_size, 0), DEFINE_PROP_UINT32("zns_num_conv_zones", FemuCtrl, zns_params.zns_num_conv_zones, 0), diff --git a/hw/femu/nvme.h b/hw/femu/nvme.h index 021efe77d23..079e40afea5 100644 --- a/hw/femu/nvme.h +++ b/hw/femu/nvme.h @@ -1761,6 +1761,7 @@ typedef struct ZNSCtrlParams { int64_t zns_status_lat; uint32_t zns_max_active; /* max active zones (0 = unlimited) */ uint32_t zns_max_open; /* max open zones (0 = unlimited) */ + uint32_t zns_num_wc; /* write caches (0 = zns_max_open, or 3 when unlimited) */ uint32_t zns_zd_ext_size; /* per-zone descriptor extension bytes (0 = none) */ uint32_t zns_num_conv_zones; /* leading conventional zones (0 = all sequential) */ uint64_t zns_zone_cap; /* usable bytes per zone; 0 = the whole zone */ diff --git a/hw/femu/zns/zns.c b/hw/femu/zns/zns.c index b4d0c1644ae..59162cd25a4 100644 --- a/hw/femu/zns/zns.c +++ b/hw/femu/zns/zns.c @@ -1794,7 +1794,20 @@ static void zns_init_params(FemuCtrl *n, NvmeNamespace *ns) /* one program covers every plane of a die; the flush walks the same set */ id_zns->program_unit = ZNS_PAGE_SIZE * id_zns->flash_type * id_zns->num_plane; id_zns->stripe_unit = id_zns->program_unit*id_zns->num_ch*id_zns->num_lun; - id_zns->cache.num_wc = ZNS_DEFAULT_NUM_WRITE_CACHE; + /* + * One write cache stages the partial stripe of one zone, so a device + * needs as many as it lets the host keep open: an open-zone limit + * exists to bound exactly these per-zone buffers (Bjorling et al., + * ATC'21). Fewer caches than concurrent writers makes every write + * evict another zone's cache and pay a synchronous flush. + */ + if (n->zns_params.zns_num_wc) { + id_zns->cache.num_wc = n->zns_params.zns_num_wc; + } else if (n->zns_params.zns_max_open) { + id_zns->cache.num_wc = n->zns_params.zns_max_open; + } else { + id_zns->cache.num_wc = ZNS_DEFAULT_NUM_WRITE_CACHE; + } id_zns->cache.write_cache = g_malloc0(sizeof(struct zns_write_cache) * id_zns->cache.num_wc); for(i =0; i < id_zns->cache.num_wc; i++) { From 9a3449362c58344c1a81bf2254abf16ad6412237 Mon Sep 17 00:00:00 2001 From: wnsah814 Date: Wed, 16 Sep 2026 13:34:11 +0900 Subject: [PATCH 2/3] femu/zns: let a write miss take an empty cache 0 The scan for a free write cache started at index 1 with cache 0 as the initial candidate, so an empty cache 0 was only picked when no other cache held more than its zero entries, i.e. never while any other cache was in use. The device therefore served one write stream fewer than it had caches: with the default 3, a third concurrent zone already evicted on every write. Scan every cache, and prefer an empty one. --- hw/femu/zns/zftl.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/hw/femu/zns/zftl.c b/hw/femu/zns/zftl.c index 985bc791c41..a74bd2c938c 100644 --- a/hw/femu/zns/zftl.c +++ b/hw/femu/zns/zftl.c @@ -503,19 +503,16 @@ static uint64_t zns_write(struct zns_ssd *zns, NvmeRequest *req) if(wcidx==-1) { - //need flush - wcidx = 0; - uint64_t t_used = zns->cache.write_cache[wcidx].used; - for(i = 1;i < zns->cache.num_wc;i++) - { - if(zns->cache.write_cache[i].used==0) - { + /* take an empty cache; failing that, evict the fullest one */ + uint64_t t_used = 0; + wcidx = -1; + for (i = 0; i < zns->cache.num_wc; i++) { + if (zns->cache.write_cache[i].used == 0) { t_used = 0; - wcidx = i; //free wc! + wcidx = i; break; } - if(zns->cache.write_cache[i].used > t_used) - { + if (wcidx < 0 || zns->cache.write_cache[i].used > t_used) { t_used = zns->cache.write_cache[i].used; wcidx = i; } From 347a8a691e3888b40e9dae4c6781fb670fd0aa76 Mon Sep 17 00:00:00 2001 From: wnsah814 Date: Wed, 16 Sep 2026 13:34:14 +0900 Subject: [PATCH 3/3] femu/zns: flush only the pages a partial write cache holds A flush walked every plane and every page of a stripe even when the cache being flushed held a handful of LPNs, as it does when a write miss evicts another zone's cache. Each such eviction burned a full stripe of physical pages (2 planes x 3 pages for one 4 KiB write in a TLC configuration), the zone's blocks ran out of pages well before the zone was full, and later LPNs were mapped past the end of their block. Reads of those LPNs then failed valid_ppa() and completed without any NAND time, so a thrashing device looked faster on reads than the NAND read latency allows. Stop the walk once the cache's LPNs are placed. The last page is still padded to a whole page, as a real program is; only planes and pages that received nothing are left untouched. --- hw/femu/zns/zftl.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hw/femu/zns/zftl.c b/hw/femu/zns/zftl.c index a74bd2c938c..1aaafa4b003 100644 --- a/hw/femu/zns/zftl.c +++ b/hw/femu/zns/zftl.c @@ -441,11 +441,22 @@ static uint64_t zns_wc_flush(struct zns_ssd* zns, int wcidx, int type,uint64_t s while(i < zns->cache.write_cache[wcidx].used) { for(p = 0;pnum_plane;p++){ + /* + * A partial cache (evicted before its stripe filled) programs + * only the pages that hold data; the untouched planes and + * pages of the stripe stay free for the zone's later writes. + */ + if (i >= zns->cache.write_cache[wcidx].used) { + break; + } /* new write */ ppa = get_new_page(zns, zone_idx); ppa.g.pl = p; for(j = 0; j < flash_type ;j++) { + if (i >= zns->cache.write_cache[wcidx].used) { + break; + } ppa.g.pg = get_blk(zns,&ppa)->page_wp; get_blk(zns,&ppa)->page_wp++; for(subpage = 0;subpage < ZNS_PAGE_SIZE/LOGICAL_PAGE_SIZE;subpage++)