Skip to content

femu/fdp: GC correctness + backpressure hardening (reworked)#194

Closed
huaicheng wants to merge 2 commits into
masterfrom
fdp-gc-followups-192
Closed

femu/fdp: GC correctness + backpressure hardening (reworked)#194
huaicheng wants to merge 2 commits into
masterfrom
fdp-gc-followups-192

Conversation

@huaicheng

@huaicheng huaicheng commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

FDP garbage-collection hardening on top of the pqueue repair in #192, split
into focused commits. Reworked from the original version of this PR: the
nrg>1 default-write-spread commit was dropped (it was incorrect, see below),
and correctness/robustness commits were added and validated on real FEMU VMs.

Commits

  1. fix cross-RG NOISY victim accounting and CB trim drain. Under
    GC_NOISY_RUH_CUSTOM with nrg>1, select_victim_ru() can return a victim
    from a different reclaim group than the caller's rgid. The global-queue
    removal, the delayed-GC put-back, the shared victim-count decrement,
    mark_ru_free(), and the implicit-RU-change event all now key off the
    victim's own reclaim group (victim_ru->rgidx). For every non-NOISY path
    this equals the caller rgid, so the default greedy path is unchanged. Also
    drains the cost-benefit victim queue in ssd_trim_fdp_style(), which
    previously leaked reclaim units across a format/trim on a CB device.

  2. run foreground GC until write pressure clears. The write path capped
    foreground GC at a fixed number of passes; it now reclaims until
    should_gc_high_fdp_style() reports the pressure has cleared, so a busy
    device waits on reclamation instead of failing a write. Bounded by the
    reclaim-unit population; exits immediately when no victim remains.

  3. report device-full instead of asserting when GC stalls. When GC cannot
    obtain a free destination RU, do_gc_fdp_style() asserted (aborting the
    emulator). It now puts the victim back on its own reclaim group's queue and
    returns -1, and check_gc_ruh_available() no longer dereferences a NULL
    curr_ru. The device degrades to a normal full outcome rather than crashing.

  4. point the initial active RU at the default reclaim group. FDP init left
    ruh->curr_ru referencing the last reclaim group's RU, but a non-placement
    write uses reclaim group 0 and the write path advances curr_ru through
    rg[rgid=0]'s bookkeeping. With nrg>1 this enqueued an rg[nrg-1] RU in
    rg[0]'s victim queue and later looked it up via its own group's queue with
    a stale heap position, a NULL dereference in victim_ru_get_pri() that
    crashed the emulator during sustained overwrites (same class as [BUG] FEMU crashed on FDP mode in victim_ru_get_pri(a=0x0) #189, but for
    nrg>1). Pointing curr_ru at rus[0] fixes the default path.

Dropped from the original PR

The nrg>1 default-write-spread commit is removed. Spreading default writes is
itself spec-legal: a write with no Data Placement Directive "uses the Placement
Handle value 0h and the controller selects the Reclaim Group for that command"
(NVMe Base Spec 2.3, Flexible Data Placement). The problem was the
implementation: it broke FEMU's invariant that each (RUH, RG) pair owns
exactly one active reclaim unit (the fallback allocated a new RU and
overwrote ruh->rus[fallback], orphaning the existing one), and it did not pin
valid placement writes to their requested group. After commit 4, nrg>1 with
default writes is crash-free but still uses only reclaim group 0. Spreading
default writes across groups, and pinning cross-group placement writes, need a
per-(RUH,RG) active-RU model (the single curr_ru pointer conflates them);
that redesign is tracked separately.

Validation

Long-run GC stress on real FEMU VMs (Ubuntu guest, KVM), FDP mode, GC tracing on:

Config Result
12 GiB, nrg=1, nru=256, ~42% fill, sustained randwrite 30,249 GCs; GC_START == GC_DONE; 0 errors
32 GiB, nru=141, pgs_per_blk=1024, ~84% fill (the #189 config) 751,820 GC_BACK_RESERT deferrals + 1,405 GCs; 0 crashes/errors
12 GiB, nrg=2, default writes crashed before commit 4 (pre-existing, reproduced on master); after commit 4: clean, GC on rg0 only, 0 errors

Total tens of thousands of clean GC cycles with no assert, segfault, or
device-full false positive.

Two FDP GC victim-bookkeeping bugs surfaced while reviewing the reclaim-unit
victim path, both independent of the issue #189 pos-aliasing crash and of the
pqueue change-priority repair (PR #192).

select_victim_ru() used the caller's reclaim group for the cross-RG cleanup.
Under GC_NOISY_RUH_CUSTOM with nrg>1 the per-RUH victim queue can hold reclaim
units from any reclaim group, yet the global-queue removal and the shared
victim_ru_cnt-- referenced ssd->rg[rgid], the caller's group, rather than the
victim's own. Remove the global twin from, and decrement the count on,
ssd->rg[victim_ru->rgidx]; this equals rgid on every path that is not cross-RG,
so single-group configurations are unaffected.

ssd_trim_fdp_style() leaked cost-benefit victims. GC_GLOBAL_CB holds its full
reclaim units in victim_ru_cb, but the trim/format drain emptied only
victim_ru_pq, so those units were never freed and victim_ru_cnt was left stale.
Drain victim_ru_cb as well. The two queues are mutually exclusive per RG mode
and share the pos index, so the inactive one is empty here; guard the shared
count against underflow.
With nrg>1 the FDP device wedged almost immediately: GC never triggered
(GC_START=0) and writes failed with "No free RUs left in rg[0] / device full",
for every GC strategy (reproduced with greedy and noisy).

Root cause: a write with no placement directive is hard-coded to reclaim group 0
(ssd_stream_write sets rgid=0 when there is no NVME_DIRECTIVE_DATA_PLACEMENT).
Each RG owns only tt_nru/nrg of the reclaim units, so rg[0] is exhausted after
filling ~1/nrg of the device -- which happens during the initial sequential fill,
before any overwrites have created GC victims. select_victim_ru() then returns
NULL (empty victim queue), do_gc_fdp_style() returns -1 without logging GC_START,
and the write path hits rg[0]'s empty free list and reports device-full.
rg[1..nrg-1] are never used by default writes.

Fix: add fdp_get_new_ru_spread(), which prefers the requested RG but falls back
to any other RG that still has a free RU, and use it ONLY on the default
host-write frontier (fdp_advance_ru_pointer RU rotation + the curr_ru recovery
path), gated by !is_placement. NVMe FDP does not bind a non-placement write to a
specific RG, so spreading default writes across RGs is spec-compliant; only the
future write frontier changes RG, already-written data keeps its RG via its PPA.
Directive (placement) writes stay pinned to their requested RG (is_placement ->
fdp_get_new_ru, no spread).

Cross-RG correctness (required once an active RU can live in a different RG than
the request's rgid):
- ssd_stream_write re-syncs rgid = ruh->curr_ru->rgidx and classifies a filled RU
  into its own RG's victim/full list (ssd->rg[ru->rgidx]); rus[] indexed by the
  new RU's rgidx.
- do_gc_fdp_style frees the victim into victim_ru->rgidx (not the caller's rgid),
  or per-RG free_ru_cnt/free_ru_list drift and GC livelocks migrating fully-valid
  RUs without restoring free RUs to the RG under pressure.
- fdp_advance_ru_pointer takes an allow_spread flag; the GC destination frontier
  passes false so GC stays pinned to the victim's RG (keeps ret_ru->rgidx ==
  dest_ru->rgidx for the rus[dest_ru->rgidx] store).

nrg=1 behavior is unchanged (the fallback loop is a no-op; rgid re-sync collapses
to the sole RG). Independently Codex-audited (a first cut that only spread without
the do_gc free-RG and GC-pinning fixes livelocked NOISY+nrg4; this version fixes
all three). Validated on flex16: greedy+nrg4 and noisy+nrg4, which previously
wedged at GC_START=0, now run cleanly under fio churn with crc32c verify, GC
triggering and completing, no #189 assertion. This also makes the multi-RG NOISY
cleanup path (victim_ru->rgidx) reachable and exercised at runtime.
@huaicheng

Copy link
Copy Markdown
Contributor Author

Reworked and landed on master as cee9670..f9fc94d4 (four commits; see the
updated description above). The nrg>1 default-write-spread commit was dropped
as incorrect. The corrected commits are validated on real FEMU VMs across tens
of thousands of GC cycles with zero errors, and they additionally fix a
pre-existing nrg>1 NULL-dereference crash (reproduced on master before the
fix). Closing this PR since the corrected work is now on master.

@huaicheng huaicheng closed this Jul 21, 2026
@huaicheng huaicheng changed the title femu/fdp: GC bookkeeping follow-ups (cross-RG accounting, CB trim drain, nrg>1 write spread) femu/fdp: GC correctness + backpressure hardening (reworked) Jul 21, 2026
@huaicheng
huaicheng deleted the fdp-gc-followups-192 branch July 21, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant