Skip to content

Moe qlc - #203

Open
Woong-DoubleK wants to merge 9 commits into
MoatLab:masterfrom
Woong-DoubleK:moe-qlc
Open

Moe qlc#203
Woong-DoubleK wants to merge 9 commits into
MoatLab:masterfrom
Woong-DoubleK:moe-qlc

Conversation

@Woong-DoubleK

Copy link
Copy Markdown

Description

Brief description of changes made.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring

Testing

  • I have tested my changes locally
  • I have added tests that prove my fix is effective or that my feature works
  • All existing tests pass
  • I have tested across multiple FEMU modes (if applicable)

FEMU Modes Tested

  • BlackBox SSD (BBSSD)
  • WhiteBox SSD (OCSSD)
  • Zoned Namespace SSD (ZNSSD)
  • NoSSD
  • Not applicable

Platform Testing

  • Ubuntu 20.04/22.04
  • Other distributions (specify): ___________
  • Build verification completed

Checklist

  • My code follows QEMU coding standards
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new compiler warnings
  • I have updated documentation if necessary
  • No trailing whitespace or C++ style comments in C files

Related Issues

Fixes #(issue number)

Additional Notes

Any additional information, configuration changes, or notes for reviewers.

Woong-DoubleK and others added 9 commits August 28, 2026 06:07
The QLC read latencies were extrapolated from TLC (Micron FMS'19) as fixed
multipliers, giving 59.33 / 85.25 / 127.20 / 169.60 us. Replace them with
measured values at 16 KB per page: 47.9 / 76.2 / 134.6 / 228.1 us.

The extrapolation is both faster in the mean (110.34 vs 121.70 us) and narrower
in spread (1 : 1.44 : 2.14 : 2.86 vs 1 : 1.59 : 2.81 : 4.76), so it understates
what page placement is worth - a bit-plane layout worth 1.346x under the measured
vector is worth only 1.221x under the extrapolated one.

Write latencies are left as the TLC extrapolation; they were not measured, and
the workload this is for writes once and then only reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things found while getting FEMU to run and measure on a shared host.

**Unpinned backing store.** init_dram_backend mlock()s the whole device and
abort()s if that fails. RLIMIT_MEMLOCK is commonly 64 MB with the hard limit
equal to the soft one, so an unprivileged user cannot raise it and cannot
emulate a device larger than 64 MB at all. FEMU_ALLOW_UNPINNED=1 downgrades the
failure to a warning; the default is unchanged, and the error message now names
the limit that would have to be raised. Pinning exists so a page fault cannot
land inside an emulated NAND access, so the variable is only sound on a host
that is not swapping - the comment says to check vmstat si/so, and notes that
swap merely occupied by stale pages is fine while active swap is not.

**The channel stage is dead code, and enabling it is not free.**
bbssd/ftl-media.c copies pg_xfer_lat into cfg.timing.page_xfer_ns and then sets
channel_mode = NAND_CH_OFF unconditionally; nand-media.c reads page_xfer_ns only
under NAND_CH_STAGED. Both call sites (bbssd, zns) select OFF, so NAND_CH_STAGED
has no users and passing pg_xfer_lat on the command line changes nothing today.

hw/femu/nand/test/test_channel.c characterises what turning it on would do. It
builds and runs without QEMU, a guest, or KVM, because nand_media_op() is pure
timing arithmetic over a caller-supplied timeline. It establishes:

  - STAGED differs from OFF even with every bus phase at zero, because the
    channel timeline is advanced to each op's data-out and the next command is
    clamped to it. So a channel_model option has to default to off, and "set the
    transfer to zero to reproduce the old numbers" does not work.
  - The staged model serialises the channel across LUNs. Reservations are taken
    in submission order, so an op's command phase waits for the previous op's
    data-out even on a different LUN. Two LUNs on one channel, both reads issued
    at t=0, slowest page: 228.1 us and 456.2 us - exactly 2x - and still 2x with
    the bus transfer set to zero, which is the clearest statement of the problem.
    Adding LUNs to a channel buys nothing; 800 reads take 139.31 ms at 1, 2, 4 and
    8 LUNs per channel, unchanged to the nanosecond. Channels do scale.
  - Consequently the page-mapping gain it reports is (mean+xfer)/(aware+xfer) flat
    at every LUN count, 1.219x for our traffic mix, rather than falling toward 1.0
    as a channel saturates. That number is a property of the model, not of a
    device.

The header comment in nand-media.c states this reproduces bbssd's
ssd_advance_status faithfully, so this is upstream behaviour rather than a defect
introduced here: FEMU models a controller that does not pipeline within a channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cycle starts at page 8, so it needs rows - 1 iterations to cover the
block; rows - 3 stopped at index 495 and left pages 496..511 holding their
zero-initialised value, which reads as QLC_LOWER_PAGE. At 256 pages per
block nothing reached those entries, so the bug was invisible in every run
taken so far and only appeared once the geometry grew to 512.

A host-side test extracts init_qlc_page_pairing from this file at build
time and asserts the class histogram, so the fix cannot silently regress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The placement work needs to know which of the four QLC page classes a read
landed on, and how many pages of each class the workload actually touched.
Counting at the NAND boundary rather than at the host queue includes
mapping-table and GC reads, which is the right boundary for NAND-core
energy: the array does that work whether or not the host asked for it.

active_ns records the raw array latency chosen for the class and excludes
queueing on purpose. Queueing belongs to the controller and would make the
per-class figure depend on the queue depth rather than on the medium.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A per-class read coefficient alone cannot say where the energy goes. The
array term scales with the number of sensing steps a class needs (1, 2, 4,
8) while the peripheral term scales with the time the page is held open, so
the two move differently as the placement changes and reporting only their
sum hides the mechanism the placement is acting on.

The array coefficient is clamped to the read total, so a mis-set pair can
only make the peripheral remainder zero, never negative. Coefficients are
device properties with the measured defaults rather than constants, and the
stats file records the pair it used so a CSV can be read years later
without the binary that wrote it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The table is what every physical-layout prediction is checked against, and a
mismatch is silent: the mapper still emits a plan, the device still serves
the reads, and only the latency is wrong. Reading it back out of a boot log
costs a boot; this costs a compile.

init_qlc_page_pairing() is static and its translation unit pulls in QEMU, so
the function text is extracted from nand.c on every build rather than copied
into the test, where the two could drift apart while still both passing.

Checked both ways: the current source gives 132/128/126/126 and passes, and
restoring the rows - 3 bound makes it fail at page 496.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both tests build outside QEMU, which is the point of them, but the recipes
are not obvious: test_channel needs a stub osdep.h because nand-media.c
includes one it will not get here, and test_pairing needs the pairing
function re-extracted from nand.c on every build so a stale include cannot
quietly test nothing. Neither is discoverable from the sources alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
176 upstream commits against this fork's 7. Eight conflicts across six files,
all of them both sides adding different things to the same place, so both sides
are kept except where the two disagree about behaviour.

  nvme.h        our exit-notifier and energy-coefficient fields alongside
                upstream's csd_ctrl_state and pe_cycles_rated
  femu.c        our process-exit/stats-timer callbacks and e_read/e_array
                properties alongside upstream's femu_realize_undo and
                pe_cycles_rated
  ftl.h         our qlc_read_* counters alongside upstream's wear totals
  bb.c          our bb_flush_stats ahead of upstream's bb_exit comments
  ftl-media.c   upstream's per-block read_cnt and its new
                bb_decode_loc(ssd, ppa, stime) signature, then our QLC
                counters, which need the decoded loc and so follow it

backend/dram.c is the one place the two disagree. Upstream now warns and
carries on when mlock fails; this fork aborts unless FEMU_ALLOW_UNPINNED is
set, and keeps doing so: the emulated latency is the measurement here, and a
page fault landing inside one is indistinguishable from the NAND time it is
supposed to be reporting. Upstream also rewrote the comment above it to
describe the warn-and-continue policy, so the comment is restated to match the
code it sits on rather than contradict it.

Verified rather than assumed, because a placement experiment fails silently:

  test_pairing passes, 132 128 126 126 -- the rows-1 QLC pairing fix survived
  the merge. Against rows-3 it reports FAIL pg 496.

  OFF-mode timing is unchanged. Upstream reworked the channel/bus model, but
  that work is inside NAND_CH_STAGED, and bbssd selects the mode from the
  channel knobs, all of which default to 0 and none of which the run
  environment sets. Replaying 4,096 reads over this geometry (2 ch x 4 LUN)
  completes at 498483200 ns on both sides of the merge, so the 24-run grid
  measured before this merge still stands.

  The merged tree builds; femu-qlc:merged carries it.

test_channel now reports six failures and they are left alone. Cases [2]-[4]
exercise NAND_CH_STAGED, whose old expectations encoded a defect -- the LUNs on
a channel used to serialise completely, which case [3] asserted while calling
it unlike real NAND. Upstream fixed that. Rebuilding those expectations is a
decision about what the staged model ought to do, and nothing here uses it yet;
the test README records why they fail so the next reader does not mistake a
known deferral for a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compose.yaml, the Dockerfile and its entrypoint have driven every measurement
on this fork but were never tracked, so the geometry, the QLC stats path and
the energy coefficients a run was given lived only in an untracked file. A
result is only reproducible if the configuration that produced it is in the
history beside the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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