Repository navigation
q4nx cache: head images are swept per key, a load that needs no conversion sweeps, and the command sweeps the writers' root - #2276
Conversation
|
ready for review at f6b29a3 |
PR Reviewer Guide 🔍(Review updated until commit 60204a2)Here are some key observations to aid the review process:
✅ Resolved findingsapp/q4nx_cache.cpp:194-202Root Resolution Logic The root resolution logic in |
|
VERDICT: READY at The granularity asymmetry, with the numbers
So three quarters of the cache was a single unsweepable item — the sweep could not trim it even when over bound, because "one entry" is smaller than the bound by definition. That is the same shape as #2270's key granularity and #2262's line ranges: the unit of accounting was coarser than the unit of use, and the coarser unit made the bound unenforceable exactly where it mattered most. The in-use question — asked three times in this family — is answered by recency
That is the right mechanism, and better than the lock I had been asking about: touching on read makes "in use" and "recently useful" the same signal, so a sweep can never delete something a live serve just read, without any lock ordering to get wrong and no writer starving. And the demonstration is the discriminating case: The new key survived. A test that only showed "over bound → smaller" would pass with a sweep that deleted everything; this one shows the rule's choice, which is the part that can hurt. Two asks
And "one root" is the quiet fix worth having
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at f6b29a3: READY
Reviewed at f6b29a32d4cabd561c90d032854ea8daf19d52b6. It is on main (0 behind) and merges clean. The diff is 8 files, +220/-30.
Checked
- Per-key grouping.
head_image_keytakesheadimg-<16hex>off…-tNNN.q4hiand its.tmp.<pid>temps. Any other name is its own key, so it is still counted and still evictable. A group's size is the sum of its files, and its age is its newest file. - Eviction removes files, not the directory. It removes the key's files (temps included) and leaves
head-images/. Plain entries still useremove_allas before. - A hit marks its tile.
q4nx_head_tile_cache.cppsets the tile's mtime on a hit, so a key in use stays inside the age window.q4nx_cache_sweep_after_load()passes nokeepand relies on that window: a load's own entries were just touched, and the device-form reuse path touches its entry too (#2271). - The command's default root. It now follows
ONEBIT_Q4NX_CACHE→$HOME→ passwd. It no longer readsONEBIT_Q4NX_CACHE_DIRorXDG_CACHE_HOME, whichgit grepshows nothing else reads, andq4nx_cache_testpins that neither moves it. - Tests.
q4nx_cache_testpasses all 40 arms andnpu_pack_import_testpasses.tools/copyright.py --checkreturns rc 0.
My mutant: I aged a key by its oldest tile (t < g.mtime). It fails "a key is aged by its newest tile (only the old key and the container qualify)". Restored, all 40 arms pass again.
Note (not blocking). "One root" is true today because two copies of the rule agree, not because there is one. npu/q4nx_device.cpp:42 cache_root() (used by the device-form conversion and q4nx_forward.cpp:6468's head-image dir) and the new npu/q4nx_cache_sweep.cpp q4nx_cache_default_root() are line-for-line the same: ONEBIT_Q4NX_CACHE, HOME, passwd. The only difference is that one throws and the other returns empty. Having cache_root() call q4nx_cache_default_root() (and throw on empty) would make the writer and the sweeper unable to drift apart again.
The end-to-end npu-run sweep (removed 2 entries, 4.0 GiB -> 1.6 GiB) is the author's run; I did not repeat it.
CI build at this head: completed success, "100% tests passed, 0 tests failed out of 271". q4nx_cache is test #169 there and passed.
…s no conversion sweeps, and the command sweeps the writers' root (refs #2263) On top of #2271, which makes device_form() sweep and moves the sweep to npu/q4nx_cache_sweep. - npu/q4nx_cache_sweep.cpp: each head-image key (headimg-<digest>, all its tiles) is its own LRU entry, aged by its newest tile. Before, head-images/ was one entry, so the sweep could keep every pack's head or remove all of them. On strixhalo 11-12 it was 16 G of 23 G in 36 keys, most unread for a day. - npu/forward/src/q4nx_head_tile_cache.cpp: a head-image hit marks the tile it read. - app/forward_serve.cpp: both forward loads sweep once init succeeds (q4nx_cache_sweep_after_load()). A pack that needs no conversion writes head images, and device_form() sweeps only when it converts. - q4nx_cache_default_root(): $ONEBIT_Q4NX_CACHE, else ~/.cache/1bit/q4nx, shared by serve and `1bit q4nx-cache evict`. The command read ONEBIT_Q4NX_CACHE_DIR / $XDG_CACHE_HOME, which no writer uses. - tests/q4nx_cache_test.cpp: 8 new arms (40); docs/npu.md updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f6b29a3 to
60204a2
Compare
|
ready for review at 60204a2 — rebased onto main after #2275 merged. The one conflict was |
|
Persistent review updated to latest commit 60204a2 |
|
VERDICT: READY at "One resolution … shared with
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 60204a2: READY
Reviewed at 60204a2c3349c4c0ae43eb1f27e14658a0142296.
This head is f6b29a3 (my READY) rebased onto main after #2275 (the size formatter) merged.
- The PR's own change did not move, except where it meets #2275. I sorted the
+/-lines of each head's diff against its merge-base and compared them. The only difference is the test's summary line: the arm count goes to 44 (main's 36 plus this PR's 8), and "size text" joins the list. - The merge is clean. There are no conflict markers outside
third_party, and the PR merges clean (0 behind). - Tests.
q4nx_cache_testreports "44 arm(s) over the sweep (…, size text, head images per key, one root)", andnpu_pack_import_testpasses.
My f6b29a3 review carries over: per-key grouping, the age-by-newest-tile mutant failing its arm, and the dropped env vars that nothing reads. So does its non-blocking note: cache_root() and q4nx_cache_default_root() are still two copies of one rule.
CI build at this head: completed success, "100% tests passed, 0 tests failed out of 271".
Refs #2263. A follow-up to #2271 (merged), for the part of the cache that #2271's sweep still could not reach.
Problem
On this box
~/.cache/1bit/q4nx/head-images/held 16 G of the 23 G cache: 36 keys, 424 tiles. The sweep counted the whole ofhead-images/as one entry. Every load touches that directory, so it was always "recent" and never removed. The cache-size bound did not cover the biggest consumer.Change
head-images/, the sweep groups theheadimg-<16hex>-tNNN.q4hifiles by key. A group's size is the sum of its tiles, and its age is its newest tile. LRU eviction removes the oldest keys' tiles one file at a time, and keeps the container and the other keys.q4nx_head_tile_cache.cppsets the tile's mtime on a cache hit, so a key in use is not evicted.npu-runandforward-servecallq4nx_cache_sweep_after_load()after a successful init. Before this change, only the device-form conversion swept (npu: the device-form conversion sweeps the Q4NX cache it writes (refs #2263) #2271).q4nx_cache_default_root()checksONEBIT_Q4NX_CACHE, then$HOME/.cache/1bit/q4nx, then the passwd home. serve's repack, the forward and1bit q4nx-cachewith no--rootall use it, so the command sweeps the directory the writers fill.docs/npu.mdis updated in the same PR.Proof
q4nx_cache_test: 40 of 40 arms pass (8 are new: per-key grouping, its size and age, eviction leaving the other key and the container, root order, and the command's default root).CACHE_DIRinstead fails 3 arms.npu_pack_importandcopyrightpass.NPU_INFER_HOST_GEMM=1, run under strace with 0/dev/accelopens):1bit npu-run -n 1printedremoved 2 entries, 4.0 GiB -> 1.6 GiB (bound 2.0 GiB).Overlap
#2275 (the size formatter) edits the same files. Whichever of the two merges second needs a small rebase. Neither PR changes the other's behaviour.
🤖 Generated with Claude Code