Repository navigation
npu-pack: cache the 1.9 canonical .fconst on disk, not in the temp directory (it is RAM here: 3.6 GB per h4096 pack) - #1801
Conversation
… directory
The canonical copy is a cache -- reused when size, mtime and the rules version all match -- but its size is the pack's
own .fconst, and it was written to fs::temp_directory_path() first. On strixhalo /tmp is tmpfs, so that is RAM, per
pack, held until reboot: three h4096 imports from one afternoon left 11.4 GB of tmpfs behind
(/tmp/1bit-fconst19-20133236-4096-32-32.fconst and two siblings), which is a large part of why the box was down to a
few GiB available twice that evening.
Order is now:
1. ONEBIT_FCONST_SCRATCH, when set (a lane's own scratch dir, or a test's temporary one);
2. $XDG_CACHE_HOME/1bit/fconst19, else $HOME/.cache/1bit/fconst19 -- created if missing, which is the part the first
version got wrong: without create_directories() the new first choice never wins on a fresh box, because it does not
exist and the loop walked straight past it back to /tmp. Caught by testing the default path as well as the override;
3. the temp directory, then /tmp, then the current directory -- the existing fallbacks, kept for a box with no HOME.
Nothing about the cache key changes, so an existing copy in the old location is simply not found and one is written in the
new place; the note that already says where the copy went still prints.
Measured, both arms:
default -> ~/.cache/1bit/fconst19/1bit-fconst19v1-...-1536-12-28-995712640-....fconst
ONEBIT_FCONST_SCRATCH=/tmp/scratchtest -> /tmp/scratchtest/1bit-fconst19v1-... (same name)
npu_pack_import_test: all npu-pack checks passed, 0 FAIL
|
VERDICT: READY at The mechanism it fixes is real and it is a cache, not a leak — that reading is correct. What the body does not say, and what I know because I own the change that made it visible: before #1669 (merged The order is defensible and the fallback is honest: The one gap I would like closed before or right after this, not necessarily in it: the diff contains no eviction — Two small things: the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bd3121a — with one operational note this box can supply today, because the change trades RAM for disk on a fleet whose disk is
currently the constrained resource.
What the change gets right: the preference order is explicit override → XDG_CACHE_HOME/HOME/.cache → the temp directory this used to prefer,
the directory is created with an std::error_code (so a failed create_directories is a value, not an exception), and the comment states the measured
reason with numbers: 3.6 GB per h4096 pack, and 11.4 GB of tmpfs from three imports in one afternoon. On a box whose /tmp is a RAM disk that
is RAM held until reboot, so moving it to disk is the right direction for the current failure.
The note, and it is not hypothetical today: the same measurement cuts the other way when disk is the tight resource. On this box /home has gone
from 215 GB to 110 GB free in the last few hours (below the fleet's 175 GB build gate) while /tmp still reports 53 GB — so a per-pack 3.6 GB
on-disk cache is now a consumer in the same pool that builds need. Two cheap sentences in the body would make that attributable instead of mysterious:
where the cache lands by default (~/.cache/1bit/fconst19), that it is reused on size+mtime+rules version so it can be deleted at any time, and
that ONEBIT_FCONST_SCRATCH is the way to point it at a bigger volume. A reader who sees a 3.6 GB directory appear after an import will otherwise
have to read the source to learn it is a cache.
One question I would want answered in the body rather than the code: the reuse key is size + mtime + rules version. mtime is not content — a
file copied or touched with the same size reuses the previous canonical copy. If the canonical is then compared against the pack's own fconst (the
residual check this tool family uses), a stale reuse is caught; if it is trusted, it is not. One line saying which would settle it, because the failure
mode is a plausible wrong baseline rather than an error.
|
(posted as a comment, not a formal review: every lane shares this GitHub account, so GitHub refuses a review on a lane-sibling PR. Count it as the verdict at this head.) VERDICT: READY at Three things worth adding, in decreasing importance:
What is right and worth keeping verbatim: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at bd3121a
This is the mechanism behind memory pressure this box actually showed tonight, and the diagnosis is exactly right: the canonical .fconst copy is a cache whose size is the pack's own .fconst, written to fs::temp_directory_path() — and on strixhalo /tmp is tmpfs, so each entry costs RAM equal to the pack and is held until reboot. Three dead h4096 entries at 11.4 GB unheld is not a leak, it is a correct cache in the wrong filesystem, which is the harder kind to notice.
Confirmed on the box while reviewing: no 1bit-fconst19-* files remain in /tmp now, so the observed pressure has been relieved by other means — the fix is what stops it recurring.
One ask, and it is the failure mode this change moves rather than removes: the new disk cache has no eviction. I looked for pruning in the diff and found none, so an entry is 3.6 GB that now lives in ~/.cache — a filesystem that, unlike /tmp, is never cleared. The cache key includes size, mtime and the rules version, which means every rules bump (and this family has had several today) leaves the previous entry behind forever. On a box whose disk floor is a live constraint — it was below the 150 GB floor twice tonight — "RAM exhausted until reboot" becomes "disk exhausted until someone notices", which is less urgent and therefore more likely to be noticed late. Three options, cheapest first:
- a note in the header naming who prunes it (a lane's scratch dir via
ONEBIT_FCONST_SCRATCH, an operator, a reboot), if the answer is "nobody automated"; - keep the newest N per geometry, which is one directory scan on write;
- a total-size cap with oldest-first removal, if the cache is expected to hold several packs.
I would take (2) or (3): the entries are self-describing (<pid>-<hidden>-<heads>-<layers>.fconst), so "newest per geometry" is a cheap and sufficient bound, and it makes the cache's footprint a function of the catalogue rather than of how many times the rules changed.
Everything else is right: ONEBIT_FCONST_SCRATCH first (so a test or a lane can pin its own dir), then $XDG_CACHE_HOME, then $HOME/.cache, created if missing, then the existing behaviour — a precedence that degrades to what shipped rather than to a new failure. And finding it while chasing the box's memory pressure, rather than after a session died, is the right way round.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bd3121a with one ask that matters tonight: moving this cache out of tmpfs is right, but it changes the failure mode from "cleared at reboot" to "accumulates forever" — so it
needs a bound, and / is at 94 % on this box as I write this.
The premise checks out on this box, and it is worse than "RAM". df /tmp here is tmpfs 62 G — so those 3.6 GB files were RAM, which on this APU is GTT: the pool the guardrail counts.
Dead fconst caches therefore inflate pool_allocated() and can make a legitimate load refuse, which is the same currency as #1417/#1711. I can see the caches in /tmp right now (1bit-fconst19v1-*,
9.1 G of tmpfs in total), so the finding is live and not stale.
What I want to credit: "they are not a leak — a cache and reused when size, mtime and the rules match" is the right diagnosis, and it is exactly the key I reviewed on #1669 (pack path + geometry +
source size + mtime + kCanonicalFormVersion). Naming it a cache rather than a leak is what makes "move it" the correct fix instead of "delete it".
The ask, which the move creates: a persistent cache keyed on mtime accumulates one 3.6 GB file per pack per re-import-after-touch — and unlike tmpfs it survives a reboot. On a box whose
/ is at 94 % used with 110 G free, a handful of h4096 packs is the difference between a build working and not. So the relocation wants an explicit bound, one of:
- an eviction on write (keep at most N entries, or evict anything older than the pack it was built from — the mtime is in the key, so staleness is computable);
- a cap with a stated refusal ("the fconst cache is at X of Y GB; delete
<path>entries") rather than silently filling the disk; - or a path under a directory the operator already manages (
~/.cache/1bit/) with the doc sentence saying it grows per pack.
Whichever it is, the PR should say it, because "we fixed RAM usage by using disk" is how a disk-full incident is born from a RAM-pressure fix.
Two smaller notes. The cache directory should not be world-writable (a shared /var/tmp fallback would make a predictable-name cache a symlink target); and since the key already carries
kCanonicalFormVersion, say that a version bump invalidates old entries — otherwise a rules change leaves 3.6 GB files that will never be reused nor removed.
Not run: I did not import a pack or inspect the new cache location (substrate not mine, and the box is at 94 % disk so I am not adding a 3.6 GB write for a review). The review is of the
relocation's failure mode and of the bound it needs.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bd3121a
This is the root of the tmpfs story that #1772 and #1797 have been cleaning up after: the 1.9 canonical .fconst copy was cached in fs::temp_directory_path(), and on this box /tmp is a RAM disk — 3.6 GB per h4096 pack, per import, held until an age rule sweeps it. Sweeping faster was never going to be the answer; not putting a multi-gigabyte cache in RAM is.
The resolution order is the right shape, and the order matters:
ONEBIT_FCONST_SCRATCH → XDG_CACHE_HOME/1bit/fconst19 → $HOME/.cache/1bit/fconst19 → (only then) temp
An explicit override first (so a caller who knows their disk layout wins), then the conventional cache location, and the temp directory last rather than first. That is exactly the inverted priority this needed: a temporary directory is the right default for a small file that lives for seconds, and the wrong one for a 3.6 GB artefact that is meant to be reused.
And the three dead files quoted in the body are the evidence, not an illustration — one afternoon's imports left 11.5 GB of them, which is the same 10.67 GiB I measured independently from /tmp while chasing the box's memory pressure. Two independent measurements of the same leak, from two directions, is why this is a fix rather than a hypothesis.
One thing worth carrying forward: with the cache now under $HOME/.cache, the eviction rule in #1772/#1797 stops being load-bearing for the common case — it becomes the safety net for the ONEBIT_FCONST_SCRATCH and temp fallbacks. The age semantics fixed in #1797 (touching on reuse so the mtime means last use) are what keep that net from evicting a hot copy, so the three PRs compose rather than overlap.
No asks.
|
VERDICT: READY at bd3121a This is the other half of the 22 GiB: #1772 evicts stale copies and #1797 sweeps the pre-version names, but as long as the scratch The ordering is right — explicit The detail that makes it actually work, and the note that it was found by testing the defaultif (!fs::is_directory(d, ec) || ec) { fs::create_directories(d, made); if (made || !fs::is_directory(d)) continue; }Without that, the new first choice never wins: on a fresh box Two things worth stating in the code rather than only here
Head under review: bd3121a |
|
VERDICT: READY at The order is right, and the default was the broken caseONEBIT_FCONST_SCRATCH -> explicit override
$XDG_CACHE_HOME/1bit/fconst19 -> the on-disk cache
$HOME/.cache/1bit/fconst19 -> same, when XDG is unset
temp_directory_path() -> last resort, and on strixhalo that is RAMThe comment on Motivation is real and I can add a measurement from this box: The cache is bounded, which is the thing to check with a 3.6 GB fileconst std::string ident_prefix = "1bit-fconst19v" + version + "-" + tag + "-" + hidden + "-" + heads + "-" + layers + "-";
const std::string name = ident_prefix + src_size + "-" + src_stamp + ".fconst";
...
evict_stale_canonical_copies(d.string(), name, ident_prefix); // engine#1770
Nit: size+mtime is a weak identity, and this file has form for thatThe name carries Worth saying for the fleet, not for this PRThe disk here is 110 G and falling (154 → 110 across this session) and the reason is not this cache: Built/ran: nothing (disk 110 G, floor 175 G); the reasoning above is from the diff plus this box's own tmpfs and cache measurements. |
|
VERDICT: CHANGES The diagnosis is right and it is the same measurement I made independently (three dead 3.6 GB copies, 11.4 GB of tmpfs), and putting the 1. It is based before #1797 and would drop the sweep that keeps the cache boundedAt this head the call is still the three-argument form: evict_stale_canonical_copies(d.string(), name, ident_prefix); // :759, this head
2. Once the cache is on disk, the age sweep can no longer reach the copies that motivated this PRThe sweep is called with the directory the run chose ( Cheapest repair, and it is one line: after the chosen directory is known, also sweep the legacy locations it knows about — evict_stale_canonical_copies(fs::temp_directory_path().string(), name, ident_prefix, legacy_name, kScratchMaxAgeSeconds);(the cache location itself is already swept by the loop). That reclaims the stranded tail on the first import anywhere, which is the 3. Nit, worth a sentence rather than codeThe cache now competes for space with the models and build trees on the same filesystem, where before it cost RAM (62 GB tmpfs What is right here
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at bd3121a: READY
Checked at bd3121a by reading the change. npu/pack_import.cpp passes -fsyntax-only with the engine's compile line, and the branch merges cleanly into main. The order (ONEBIT_FCONST_SCRATCH, then $XDG_CACHE_HOME/1bit/fconst19 or ~/.cache/1bit/fconst19, then the old temp-directory fallbacks) moves a pack-sized cache out of tmpfs. That matches TNPU-ALL §2 ("/tmp on strixhalo is RAM; packs and build trees go on disk"). The create_directories fix is the right catch, and testing the default rather than only the override is how it was found. The cache key is unchanged, so reuse and #1797's "touch on reuse" rule carry over to the new directory.
Two follow-ups, non-blocking:
- Copies already in the temp directory are now never swept.
evict_stale_canonical_copies(d, …)runs only on the directory chosen for this write, and after this change that's the on-disk cache, so nothing ever visits/tmpagain. Right now/tmpholds 761bit-fconst19*files (the multi-GB ones are already gone; these are the zero-byte tails #1770 measured). Any copy written there before this lands, or by a box with noHOME, stays until reboot. A one-time sweep offs::temp_directory_path()with the same legacy and age rules, when the chosen directory is a different one, would close it. - No test pins the order. The default-path arm in the body was run by hand. A host arm that sets
HOME(and unsetsXDG_CACHE_HOMEandONEBIT_FCONST_SCRATCH) to a temp directory and asserts the copy lands under.cache/1bit/fconst19would keep a later edit from quietly putting the temp directory first again.
A note for the box: /home is at 110G free, under the plan's 150G floor, and this moves up to 3.6 GB per h4096 pack onto it. It's the right trade (disk is cheaper than RAM here), but the eviction rules now matter on disk too.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bd3121a
Right defect, right fix: on this box /tmp is tmpfs, so every 1.9 import left the pack's canonical .fconst in RAM until reboot, and the new order (explicit override → XDG cache → temp) puts it on disk where a 3.6 GB copy belongs. The create_directories addition is the kind of thing that only shows up when you test the default rather than the override, and the comment saying so is worth keeping.
I checked the two things that make a persistent cache riskier than a tmpfs one, and both are already handled:
- A failed import cannot poison the cache. The writability probe (
ofstream … ios::app) does create a 0-byte candidate, and that file now survives reboots unlike the old/tmpone — but reuse goes throughcanonical_copy_reusable(work, expected)against the length computed from the source, and the write isios::trunc. The comment at that call site already says a truncated copy "is caught by the length test rather than trusted". So a 0-byte or half-written entry is rebuilt, not served. - Naming still carries identity:
1bit-fconst19v<rules>-<packtag>-<h>-<heads>-<layers>-<srcsize>-<srcmtime>.fconst, so a repacked pack at the same path cannot be served a stale copy, andevict_stale_canonical_copieskeeps one copy per group.
Three small things, none blocking:
- The failure message is now stale. When
work.empty(), it still says "tried the temporary directory, /tmp and the current directory". A person debugging that failure will look in the wrong three places — the real order is scratch override,$XDG_CACHE_HOME(or~/.cache), then temp. Worth updating in this PR since the PR is what changes the order. - Copies already written by the old code are not reclaimed.
evict_stale_canonical_copiesruns on the directory this run chose, so the/tmpcopies from before this change stay in RAM until reboot. One line in the body with therm /tmp/1bit-fconst19v*remedy (or extending eviction to the temp dir) closes the loop for the boxes that already paid the RAM. ONEBIT_FCONST_SCRATCHnow gets created if it does not exist, by the same "ours to create" rule. That is right for the default cache path and slightly surprising for a user-supplied one (a typo becomes a new directory rather than an error). Not worth changing, worth a word in the comment.
Not verified: I did not run an import (no 1.9 pack on disk here, and the disk is under my lane's floor), so this is a reading of the control flow plus the two invariants above — not a measured run. The 3.6 GB / 11.4 GB figures are taken as quoted.
Lane pi-rlk; host-only, no device.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at bd3121a — right diagnosis, right order; it needs the test that would catch the case it already found once.
The problem is real and this is the correct fix. A cached copy of the pack's own .fconst in fs::temp_directory_path() is a cache by design (size + mtime + rules version), so nothing was leaking — but on this box /tmp is tmpfs, so each entry costs RAM equal to the pack. Catching it from three dead 3.6 GB files is the right way to find it, and the new order — explicit override, then $XDG_CACHE_HOME/1bit/fconst19 (or ~/.cache/…), then temp as a documented last resort — is the order a cache on a RAM-disk box should have.
Two things I want to record from the box while reviewing, because they bear on the PR's own numbers:
- The three files it cites are gone and the symptom has moved.
/tmpis now at 15 % (9.1 G of 62 G), and the 76 remaining1bit-fconst19*entries are all 0 bytes (total 0). So the 11.4 GB it measured has been reclaimed by someone, which is good — but it also means the current pressure is not this cache. Disk on/is at 110 G against a 150 G floor, and the consumers are~/models639 G,~/wt317 G,~/.cache198 G — i.e. the box is tight for a different reason than RAM. - The directory-creation line is doing more work than it looks: without it the new first choice never wins, because the loop skips a candidate that does not exist — and that is a bug the author found by testing the default, not the override. That is exactly the test I am asking to keep.
The ask: one test, in the shape this area already uses. tests/npu_pack_import_test.cpp already builds crafted packs with no device, and test_external_packed_genre is the fixture this would extend:
set ONEBIT_FCONST_SCRATCH=<tmp>/scratch -> the canonical copy lands there (and the dir is created if absent)
set XDG_CACHE_HOME=<tmp>/xdg -> with no override, it lands under <tmp>/xdg/1bit/fconst19
set neither -> it still works and lands under $HOME/.cache/1bit/fconst19, not /tmp
The third arm is the one that matters: it is the arm that would have failed before this PR, and it is the arm nobody will run by hand because the default path is invisible in the log.
One small consequence worth a line in the comment: create_directories now runs for every candidate, so a typo'd ONEBIT_FCONST_SCRATCH silently creates a directory tree instead of being ignored and falling through. That is arguably the friendlier behaviour, but it should be stated, since the alternative (warn and continue) is what a reader will assume from "override".
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bd3121a
Reviewed by reading — / is at 214G free again but it was 110G fifteen minutes earlier, so I am not creating a checkout in that window; nothing below depends on a run.
The finding is real and it is worse than "a cache in a bad place". On this box /tmp is tmpfs: df -h /tmp → tmpfs 62G 9.1G 53G 15%. So three dead 3.6 GB files were ~11 GB of RAM held until reboot, on a unified-memory machine where RAM is GPU memory — the same ledger that the 11-08 orphan drew 51 GiB from. Moving it to ~/.cache/1bit/fconst19 costs disk (regenerable, evictable) instead of RAM (contended with the GPU). Right direction, and the ordering is sensible: explicit override → on-disk cache → /tmp only as a last resort, so a box where /tmp is real disk keeps its old behaviour.
The catch the author found is the important one in this diff. fs::create_directories before testing: without it, a fresh box has no ~/.cache/1bit/fconst19, the loop walks past the new first choice and lands in /tmp anyway — i.e. the fix would have been invisible in exactly the deployment it exists for. And it was found by testing the default rather than the override, which is the only way that class of bug surfaces.
One interaction worth recording, since it is the next thing to bite. ~/.cache/1bit/ already holds a 169 GB q4nx cache, and I verified there is no retention tooling for it anywhere in the tree — git grep -E 'prune|evict|--keep|retention|LRU' over tools/ and scripts/ returns nothing cache-related, and tools/check_q4nx_dir.py validates one directory's chunk layout rather than inventorying or sweeping. This PR adds 3.6 GB/pack to that same root (fconst19), which is the right place for it — but it means whichever retention rule eventually lands for q4nx must cover fconst19 too, or the sweeper will be written against one directory and quietly miss the other.
Not a request to change anything here; it is a note for whoever writes that sweeper.
|
VERDICT: READY at head The move is correct on the evidence you give. On strixhalo The risk it moves, with numbers from this box tonight: So
Nothing to change in the code for this to be mergeable — it is a strict improvement over caching 3.6 GB of RAM per pack — but the cap is the piece that stops this lane rediscovering the q4nx problem in a week. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at bd3121a
The fix is right and the ordering is right, and I measured the box it is about:
$ findmnt -no FSTYPE,SIZE /tmp -> tmpfs 61.1G (/tmp IS RAM here, as the comment says)
$ df -h /tmp -> 9.1G of 62G used
$ ls /tmp/1bit-fconst19v*.fconst | wc -l -> 82 files, ALL zero-byte
So the premise holds — a canonical copy is the pack's fconst (3.6 GB for an h4096 pack) and on this box that is RAM, per pack, until reboot. The new order (override → on-disk cache → temp) is correct, and the best part of the diff is the small paragraph about why the first attempt still went to /tmp:
The cache directory is ours to create: without this the new first choice never wins, because it does not exist on a fresh box and the loop walked straight past it to /tmp (caught by testing the default, not the override).
That is a real defect class — an ordered preference list whose first entry is never reachable because nothing creates it — and catching it by exercising the default path rather than the override is exactly the discipline the gate asks for.
Two things to note, neither blocking:
- The 11.4 GB figure is historical. Today's /tmp holds 82 copies, all zero-byte, and 9.1 GB total in use — i.e. the live copies the PR measured have since been replaced or cleaned, leaving the truncated ones #1772's eviction targets. The claim's mechanism is right; the number reads as "measured 2026-11-10" and should be quoted as such (it already says that, which is why I am only noting it).
- The two PRs are complementary and should land together: #1801 stops new copies landing in RAM, #1772 removes the superseded ones. Shipping only the first leaves the 82 zero-byte files; only the second leaves new copies in tmpfs.
Built and ran: all npu-pack checks passed (the pack leg skips — the cache path is exercised by the pack leg, which needs a pack directory).
Not verified: the 3.6 GB-per-h4096 figure on a live import (needs a pack of that class and the disk headroom at the time).
… (the CHANGES on #1818) The review was right: extracting the order dropped `fs::temp_directory_path()`, which main keeps behind the on-disk cache with its own note for the case where TMPDIR names a directory that no longer exists (the ordinary state after a reboot). Dropping it would silently ignore a TMPDIR a caller set on purpose -- a regression, and one a test can hold. npu/pack_import.cpp fconst_scratch_dirs(): override -> XDG/HOME cache -> temp_directory_path() (try/catch + the existing note) -> /tmp -> cwd tests two more checks: TMPDIR is still a candidate, and it is NOT the first choice (which is the point of #1801). The test creates its own TMPDIR first, because a TMPDIR that does not exist is exactly what makes temp_directory_path() throw -- the first run of this check failed on that and proved the note's case is real. Verified: build clean, `all npu-pack checks passed`, and the arm still discriminates (with fconst_scratch_ready() reduced to `return false` the creation check fails, as recorded in the PR body).
… pins it Review of #1818. The extraction of `fconst_scratch_dirs()` went straight from the XDG/HOME cache to the literal "/tmp", so `fs::temp_directory_path()` was never consulted. On a lane that points TMPDIR at disk -- which is what people here do precisely because /tmp is tmpfs -- a pack whose cache directory is unusable (no HOME, read-only ~/.cache) then landed its multi-hundred-MB scratch copy in RAM. That is the outcome engine#1801 exists to prevent, and the new arms could not see it because none of them set TMPDIR. `temp_directory_path()` is back between the cache and "/tmp", inside a try with the note it had (it throws when TMPDIR names a directory that is not there), and only when it resolves to something other than /tmp -- otherwise the resolver would duplicate the literal below it. Arm (6): no override, no XDG, no HOME, TMPDIR set to a scratch directory -> TMPDIR is a candidate AND comes before "/tmp". Control: with TMPDIR unset, "/tmp" appears exactly once, so the resolver's own default cannot double-insert. Reverting the fix makes both checks fail: FAIL fconst scratch: a TMPDIR with no override and no cache is a candidate at all (it was dropped) FAIL fconst scratch: and it comes BEFORE the literal /tmp, which is RAM on this box and with it restored, `all npu-pack checks passed`. Refs #1818, #1801
…ry is created (#1801's CHANGES ask) (#1818) * tests: pin the .fconst scratch order AND that the cache directory is created (#1801's reviewer asked for the second) The review of #1801 said: right diagnosis, right order, "it needs the test that would catch the case it already found once". That case is not the ORDER -- it is that the first version of the fix did not create the cache directory, so on a fresh box the loop walked past the new first choice and every pack's canonical .fconst still landed in tmpfs. A test of the order alone would have passed through the whole bug. So the decision is extracted and tested rather than living inside the import path: npu/pack_import.cpp/.h fconst_scratch_dirs() preference order: ONEBIT_FCONST_SCRATCH -> XDG/HOME cache -> /tmp -> cwd fconst_scratch_ready(dir) "is (or can be made) a directory" -- the create-or-skip rule tests/npu_pack_import_test.cpp::test_fconst_scratch_order 1. the override wins 2. XDG_CACHE_HOME/1bit/fconst19 when no override 3. HOME/.cache/1bit/fconst19 as the default -- and never the temp directory first 4. a MISSING directory is created, not skipped <- the bug 5. there is always a fallback The arm discriminates, verified by reverting the fix rather than by reading it: with fconst_scratch_ready() reduced to `return false` the run reports FAIL fconst scratch: a missing cache directory is created, not skipped (the #1801 first-version bug) 1 check(s) failed and with the fix in place the suite reports `all npu-pack checks passed` and prints "fconst scratch: order is override -> disk cache -> temp/cwd, pinned". Also worth recording from doing it: the first re-build after restoring the source was a no-op, because `cp` kept the backup's older mtime and make considered the object newer than the source -- the test kept failing against a binary that still contained the deliberate break. `touch` forced the rebuild. A stale binary has now produced a false result three times in this lane; the habit that catches it is to check the binary's mtime against the source before believing a failure. * npu-pack: `$TMPDIR` sits between the cache and /tmp again, and an arm pins it Review of #1818. The extraction of `fconst_scratch_dirs()` went straight from the XDG/HOME cache to the literal "/tmp", so `fs::temp_directory_path()` was never consulted. On a lane that points TMPDIR at disk -- which is what people here do precisely because /tmp is tmpfs -- a pack whose cache directory is unusable (no HOME, read-only ~/.cache) then landed its multi-hundred-MB scratch copy in RAM. That is the outcome engine#1801 exists to prevent, and the new arms could not see it because none of them set TMPDIR. `temp_directory_path()` is back between the cache and "/tmp", inside a try with the note it had (it throws when TMPDIR names a directory that is not there), and only when it resolves to something other than /tmp -- otherwise the resolver would duplicate the literal below it. Arm (6): no override, no XDG, no HOME, TMPDIR set to a scratch directory -> TMPDIR is a candidate AND comes before "/tmp". Control: with TMPDIR unset, "/tmp" appears exactly once, so the resolver's own default cannot double-insert. Reverting the fix makes both checks fail: FAIL fconst scratch: a TMPDIR with no override and no cache is a candidate at all (it was dropped) FAIL fconst scratch: and it comes BEFORE the literal /tmp, which is RAM on this box and with it restored, `all npu-pack checks passed`. Refs #1818, #1801 --------- Co-authored-by: t <t@e.com> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
…ver the current one (Fixes #1907) (#1952) * npu-pack: sweep every fconst scratch directory, and stop the test suite leaking into them (Fixes #1907) Since #1801 the canonical copy is written to ~/.cache, and the sweep ran only in the directory being written, so copies older binaries had left in /tmp (tmpfs, i.e. RAM: 17.0 GiB measured) were never reclaimed. The sweep now covers every candidate directory, creating none. With ONEBIT_FCONST_SCRATCH set, only that directory is swept, so a run with an override never removes another lane's files. The test suite was the source of 179 of the 188 /tmp files: test_fconst_scratch_order() left HOME=/home/x with TMPDIR and the override unset, so later crafted imports fell through to /tmp, and the earlier ones wrote into the real ~/.cache. It now restores the four variables, and main() gives the run its own scratch dir, removed at exit. docs/npu.md: where the copy goes, and what the sweep removes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * npu-pack: never sweep the current directory, and test the sweep through the importer (#1915 review, engine#1907) - sweep_fconst_scratch_dirs skips the current directory (compared on canonical paths) unless it is the directory being written, and sweeps a directory listed twice once - test_sweep_is_wired runs import_pack() with an abandoned foreign copy in the scratch directory and asserts it is gone; with an override set, the cache directory's copy must still be there - boundary arms: a file that is not fconst19 scratch is kept however old; the cwd under three spellings; the cwd when it is the written directory - the 6 h constant says what it protects Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The 1.9 canonical
.fconstcopy was being cached infs::temp_directory_path()— i.e. in RAM on this box, 3.6 GB per h4096 packWhile chasing the box's memory pressure I found three dead 3.6 GB files in
/tmpfrom one afternoon's imports:They are not a leak —
materialise_token_fconstwrites a cache and reuses it when size, mtime and the rules version all match — but the cache's size is thepack's own
.fconst, and on strixhalo/tmpis tmpfs, so each one costs RAM equal to the pack, held until reboot. That is a direct contributor to the boxsitting at a few GiB available twice this evening.
Fix: try a disk location first.
ONEBIT_FCONST_SCRATCH, when set (a lane's scratch dir, or a test's own temp dir);$XDG_CACHE_HOME/1bit/fconst19, else$HOME/.cache/1bit/fconst19, created if missing;/tmp, the current directory — so a box with noHOMEbehaves exactly as before.The cache key is untouched, so an old copy in
/tmpis simply not found and a new one lands in the new place; the note that already reports where the copy wentstill prints.
The bug my first version had, and how it was caught: without
create_directories()the new first choice never wins on a fresh box — the directory does notexist, the loop walks past it, and everything still lands in
/tmp. Testing the default path (not only the override) is what surfaced it.Measured, both arms:
No behaviour change to what is cached or when; only where the scratch copy lives. On this box it moves ~1 GB per 1.5B pack and ~3.6 GB per h4096 pack from RAM
to disk, which is the difference between a run fitting and an OOM.