Moe qlc - #203
Open
Woong-DoubleK wants to merge 18 commits into
Open
Conversation
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>
compose.yaml carried the pilot geometry -- 7,168 MiB over 8 channels x 8 LUNs at 256 pages per block -- while every measurement in this repository was taken on 64 GiB over 2 channels x 4 LUNs at 512 pages per block with op_pcent=7. The harness passes those explicitly, so its runs were right, but a bare `docker compose up` emulated a different device and said nothing about it. The page count is the part that bites. Upstream's QLC pairing table only assigns classes for pages 0..495, so at 256 pages per block every class is correct and the bug this fork fixes (nand.c, rows - 1) cannot appear. Someone reproducing at the old default would run this source and see physics it does not model, with no error anywhere. Defaults only. The run harness exports these before calling compose, so nothing about the runs already recorded changes; what changes is what happens when a reader sets nothing, which now reproduces the measured device instead of quietly substituting another one. Verified by starting a container with no overrides: SSD_SIZE_MB=65536 CH=2 LUN=4 PPB=512 BPP=1024 CELL=4 OPTS=op_pcent=7 MEM=8G. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The emulator was here; what drives it was not. Every result in this fork came out of a set of scripts that lived only on one host's filesystem, untracked, so a clone could build the device but had no way to fill it, prove the placement landed, or replay anything against it. Only the FEMU-host half is committed. The GPU host's analysis, plotting and quantiser tooling stays there: it needs the model weights and a GPU, and the two halves being one rsync'd directory is what let an edit to run_policy.sh get reverted twice mid-sweep. Splitting them by the machine they run on is the point, not a side effect. The layout is mirrored rather than flattened because the scripts resolve each other by relative path -- run_device.sh takes its root three levels up, and femu_compose.sh one level up from itself. run_device.sh gains "$ROOT/.." as a FEMU checkout candidate, which is this arrangement: the harness inside the emulator's tree, where before the emulator sat under the harness's _deps. The blanket *.md in .gitignore, which arrived with the QEMU 10.1.0 upgrade, would have dropped the README explaining the procedure. Excepted rather than force-added, so the next document does not vanish the same way. Not committed because it is data, not code: the payload (about 15 GB), the collected traces, the layouts built from them, and a guest image. The README says so and says what shape they take. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The harness resolved one root from its own location and used it for both the scripts it calls and the images, guest binaries and payload packages it reads. That was fine while the harness was the project, but it is now inside the FEMU checkout, and the data -- tens of gigabytes, distributed separately -- is not. Resolving both from the same place meant moving the code silently took the data lookups with it. HARNESS is where these scripts are. ROOT is the project holding the data, and FEMU_PROJECT_ROOT sets it. It defaults to HARNESS, so unpacking the data under the harness, which is what a clone gets, still works with no argument. Verified from the published layout with the data elsewhere: HARNESS resolves to the harness, ROOT to the data project, and every reference on both sides -- guest_replay.sh, make_seed.py, femu_compose.sh, the FEMU checkout, the images, the built guest binaries, run01.env, the packages and the records -- resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mapper, the trace compiler, the layer-group builder and bundle.py were copied in with the harness. They belong to MoE_Trace, and keeping a second copy here would rebuild the problem the split exists to end: two owners for one file, which is how a pair of mapper copies came to sit 64 lines apart. Nothing in the harness calls them, so this costs the run path nothing. The set was incomplete here anyway -- layer_read_groups.py imports online_cache, which was never copied across, so it could not have run. REPLAYER_V1.md stays. It specifies the binary format, and the program that consumes that format, replay_v1.c, is in this repository. The README now names all three owners and says the part that would otherwise be found the hard way: building an image needs MoE_Trace's mapper. The harness runs without it once an image exists, which is why nothing here breaks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_device.sh and drive_multi.sh already told HARNESS from ROOT; the sweep path still resolved one root for both. Moving the harness out of the data project would have taken its code lookups along and left the sweep calling scripts that are no longer where it looked. run_sweep.sh finds its siblings under HARNESS. run_policy.sh does the same for guest_replay.sh, make_seed.py, femu_compose.sh and the driver, while the image, the guest binaries and the replay binary stay under ROOT. drive_run.sh touches only counters and records, so it follows ROOT alone. Each also accepts the FEMU checkout as the harness's own parent, which is how the published layout sits. preflight.sh guards a code file, so its target follows HARNESS. Worth saying plainly: this guard exists because a sync reverted run_policy.sh twice, and git supersedes it the moment that sync stops carrying the harness -- a checkout that is overwritten shows up in git status. Until then it still earns its place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README's build-and-run is not available on the host this fork is measured on, and until now the alternative was a compose invocation with six environment variables, which looks like a different project rather than the same one. femu-docker.sh is that path with the README's steps and names: build, verify, run, ssh, stop. Its header and the new README section give the mapping, so the question it kept raising -- why not just use run-blackbox.sh -- is answered where it gets asked rather than in a conversation. The reason is worth stating once here too. Two of the README's steps need root: pkgdep.sh installs packages, and run-blackbox.sh launches QEMU under sudo because FEMU pins its memory backend. Pinning needs RLIMIT_MEMLOCK past the device size; this host allows 64 MiB against 64 GiB, and has no passwordless sudo to raise it. The container gets IPC_LOCK and an unlimited memlock without the host granting root to anyone. Two things the README leaves to the reader are handled, because both are ways to lose an afternoon: the guest disk is a copy-on-write overlay per instance rather than the base image written in place, and the seed authorises an ssh key -- the cloud image ships no password, so without one there is no way in. It sits beside the scripts it mirrors, in hw/femu/scripts, which the femu-scripts symlink at the root points at. Checked against a running container: verify lists the femu device options, and status reports cell=4 size=65536MB 2ch x 4LUN 512pg/blk 1024blk/pl opts=op_pcent=7, which is the measured configuration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous message gave one reason for two different situations, and it was only right about the first. sudo and RLIMIT_MEMLOCK are why the README's path cannot run on this host. They say nothing about why it is not what runs inside the container, where the process is already root -- sudo is not even installed there, so run-blackbox.sh would fail for want of a binary, not a privilege. The actual reason is that run-blackbox.sh writes the SSD layout into itself: pgs_per_blk=256, luns_per_ch=8, nchs=8, ssd_size=12288 and a fixed u20s.qcow2, none of which is this fork's device, and it reads no environment. A run cannot be handed its geometry, its guest disk, its payload disk or its counter path. femu-run is the same script with those constants lifted out. Both reasons now sit in the README and the script header, separately, so neither is used to argue something it does not support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ironment The SSD layout was written into the file, so running a second configuration meant editing the script or keeping a forked copy of it per device. That is why this fork ended up launching through a separate program instead: not because the script did the wrong thing, but because there was no way to hand it anything. Every constant is now an environment override with its former value as the default, using the same names the container path already uses, so one description of a run works whichever way it is started. Added the same way, appended only when set: the NAND cell type, the read-energy coefficients, any further device options, and extra read-only drives -- a replay payload is far too large for a cloud-init seed and has to arrive as a disk. Checked rather than assumed: with an empty environment the composed device string is byte-identical to the one this script produced before. With this fork's settings it matches what the container path builds, down to the energy coefficients and op_pcent=7. Two things that were not configuration but blocked reuse. sudo is how an ordinary user reaches KVM and raises RLIMIT_MEMLOCK for the pinned backend; in a container the process is already root and sudo is usually not installed, so it is skipped when the caller is root. And the binary is looked up on PATH when the build-femu copy is not beside the script. One interaction worth recording: pg_rd_lat stays in the command line for compatibility, but setting a cell type turns the flat timing off (ftl-media.c:147) and the per-page-class table replaces it, so on QLC the flat latency is inert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The container path existed but the way to use it did not, beyond a compose invocation with six environment variables buried in a conversation. RUNNING.md is the two entry points with their actual output: a device to poke at, and a measurement. Every command in it was run on the measurement host and the quoted output is what came back, including the guest reporting nvme0n1 as 59.8G -- which is the 64 GiB device with op_pcent=7, and the quickest way for a reader to tell they got the right one. The failures worth naming are named, because each cost time here first. The guest's login prompt appears minutes before ssh works, since cloud-init installs the key afterwards; the seed builder needs pycdlib, which the system interpreter has and a venv usually does not; and image refuses to overwrite an instance's disk rather than silently discarding a guest. The blanket *.md in .gitignore swallowed this file too, the same way it nearly swallowed the harness README. Root-level markdown is now excepted rather than force-added, so the next document beside README.md does not vanish without a word. docker-data/, which the container writes into beside the checkout, is named as ignored instead of sitting untracked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Brief description of changes made.
Type of Change
Testing
FEMU Modes Tested
Platform Testing
Checklist
Related Issues
Fixes #(issue number)
Additional Notes
Any additional information, configuration changes, or notes for reviewers.