feat: loader-aware background prefetch (cattensors, integrated) - #30
Open
GencSaracaydin wants to merge 12 commits into
Open
feat: loader-aware background prefetch (cattensors, integrated)#30GencSaracaydin wants to merge 12 commits into
GencSaracaydin wants to merge 12 commits into
Conversation
Ports the useful half of the internal cattensors prewarmer into flashpack and couples it to the parallel reader so a warm can never race a load or flip its O_DIRECT gate: - flashpack.prefetch_flashpack_file(path): cancellable chunk-parallel buffered page-cache warm, residency-gated (>=0.9 no-ops), idempotent per path via a process-wide registry. Start it before imports/setup work; the load then reads a hot cache at memory speed. - parallel_read_into_storage() settles any registered prefetch before choosing an I/O path: done -> hot-cache buffered via the mincore gate; nearly done (>=0.9) -> bounded wait; barely started -> cancel and take O_DIRECT. Partial warms and evictions self-correct because the gate re-checks residency itself. - read_flashpack_file() gains eager_cpu: explicit per-call choice of eager parallel CPU materialization vs lazy mmap (None defers to the FLASHPACK_CPU_PARALLEL_READ opt-in). revert_from_file() defaults to eager: reverting reads every tensor by definition. - scripts/bench_prefetch.py: overlap/settle/hot scenarios, bit-verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
…ndings) Adversarial review fixes on the prefetch integration: - fork safety: os.register_at_fork clears the registry and re-arms the locks in children (reader threads never survive a fork; an inherited handle could stall settle 120s or deadlock on a lock held at fork) - cancel/_start race: thread spawning is serialized against cancel via a spawn lock, so settle can never join an unstarted thread (RuntimeError) and a cancelled handle never spawns readers - bounded everything: cancel() takes a join_timeout; the settle path uses it (15s) so a reader wedged in preadv on a dead mount is abandoned as a daemon thread instead of hanging the load - settle gates on RESIDENCY, not bytes-read: a large warm that already evicted its own head is cancelled (waiting would buy a stale cache and lose the O_DIRECT path) - prefetch skips files larger than available physical memory (a warm that evicts itself is pure waste) - registry: keyed by realpath (symlink aliases share one warm); settled handles are replaced instead of pinning the path forever; settle keeps the handle registered while settling so concurrent same-path loads all settle the same warm - revert_from_file default back to lazy (eager_cpu=None): eager is now strictly opt-in — full-pack RSS is the wrong default for streaming consumers like the CLI unpack path - tests: 10 new cases (fork child, concurrent prefetch, prefetch-vs-load race, residency-vs-progress gate, bounded wait timeout, memory guard, symlink alias, cold natural completion, re-prefetch semantics, gpu- marked CUDA settle) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
- --net-cold-packs: copies never read by this node, one scenario each, run first (any full read warms the mount cache for that file forever) - --expect-checksums: bit-verification against sums computed by the seeding node, so the reference load cannot warm the bench node - record page-cache residency right before each post-prefetch load (diagnoses mounts that do not retain the cache across opens) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
- --juicefs-bin enables per-scenario mount-cache eviction (--evict) and tier labeling (--check: LOCAL/REMOTE/object-store) so net-cold and object-cold rows are enforced+verified, not assumed from job placement - --object-cold: locally-written copy purged from the mount cache -> no DC peer holds it -> true object-store-cold loads Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
Contributor
Author
…no-op The prefetch module deliberately no-ops off POSIX (pre-completed handle, zero reads). Four tests staged a LIVE warm and asserted its mechanics — impossible on windows-latest. Mark them posix_only and add a platform- independent test pinning the documented non-POSIX behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
Contributor
Author
|
@codex review |
…ernal setup-time harness The measured numbers in the PR description stand (methodology documented there); the harness itself will live in a general setup-time comparison tool rather than this repo. No changes to package code or tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
Contributor
Author
|
self review: should remove the benchmarking references as a seperate harness will be handling that |
GencSaracaydin
commented
Jul 21, 2026
GencSaracaydin
left a comment
Contributor
Author
There was a problem hiding this comment.
There are windows fallback features (like parallel_read available which are completely unnecessary, remove those)
…ux) runners - _read_chunk_buffered preadv-only (no pread fallback) - no non-POSIX no-op branch in prefetch_flashpack_file - register_at_fork hasattr kept strictly as import-safety for the packing side of the repo, which still CI-tests Windows - drop the non-POSIX-behavior test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
sysconf(SC_AVPHYS_PAGES) counts only FREE pages; on a long-running node
the page cache keeps that near zero, so the oversized-file guard
spuriously no-oped every prefetch. Caught live on a production H100
against qwen-image-2512s real 25 GB pack (prefetch reported done with
zero reads; the load ran fully cold). Use /proc/meminfo MemAvailable,
which includes reclaimable cache. Also expose handle.skipped_reason
("hot"/"memory"/"empty") so a silently-skipped warm is diagnosable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
The warm has the whole import/setup window; it does not need to saturate. Every block it pulls is also written to the mount local cache, so an aggressive warm competes with co-located reads (cold venv imports, neighbors) on the same NVMe. A/B on production H100s against qwen-image-2512s real 25 GB pack: 2 threads gave at least the load benefit of 8 (26.4-27.9 s vs 33.1 s cold loads) with far less collateral I/O; the one catastrophic sample (189 s imports) came from the aggressive default overlapping a cold venv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
…switch only Addresses the PR review: the engine targets fal Linux runners, so the POSIX platform term (and the helper that existed to carry it) is gone. parallel_read_supported and the eager_cpu override now consult only the FLASHPACK_PARALLEL_READ kill switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
Contributor
Author
|
Addressed across three commits:
The one platform-conditional left is the |
test_cuda_posix_default_on asserted the old posix gate that 188c7dc removed; the contract is now "on by default, FLASHPACK_PARALLEL_READ is the only switch". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK
This was referenced Jul 25, 2026
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.
feat: loader-aware background prefetch (cattensors, integrated)
Why
fal's registry spent June–July bolting an external page-cache prewarmer (
cattensors) onto ~50 apps, several of which point it (or vendored copies of its reader) at.flashpackfiles. That composition is exactly what #28's gate fix made hazardous: an uncoordinated warm makes pages resident, the loader's mincore gate then legitimately picks buffered reads — but the warm itself cost a full extra pass over the file, and if the load starts mid-warm the two race each other for filesystem bandwidth with the gate frozen on a stale answer. Registry apps meanwhile keep a real need the warm was serving: overlapping the pack read with imports/setup work (e.g.qwen-image-2512vendors a copy of the cattensors reader solely to warm the pack its LoRA manager re-reads on CPU).This PR moves that capability inside flashpack, where it can cooperate with the loader instead of fighting it.
What
flashpack.prefetch_flashpack_file(path) -> FlashpackPrefetch— a cancellable, chunk-parallel, buffered page-cache warm to start before imports/setup work:The reader settles the prefetch before choosing an I/O path (
parallel_read_into_storage): finished → the mincore gate sees a stable hot cache and routes buffered at memory speed; ≥90 % done → bounded wait for the remainder; barely started → cancel (join) and take O_DIRECT cold, which is faster from there. Partial warms and post-warm evictions self-correct because the gate re-checks residency itself. A warm can no longer race a load or flip its gate mid-flight — by construction.read_flashpack_file(..., eager_cpu=)— per-call choice of eager parallel CPU materialization (True), lazy mmap (False), or theFLASHPACK_CPU_PARALLEL_READenv opt-in (None, default; unchanged behavior).revert_from_fileaccepts the same knob (default stays lazy — full-pack RSS is the wrong default for streaming consumers like the CLI unpack path; passeager_cpu=Truefor full-read consumers, where #28 measured the eager path 2.2× faster cold).Measurements
H100 fal runners, JuiceFS
/data, 24 GB pack,device=cuda, 10 s of simulated setup work, every load bit-verified against checksums computed by a different node (seederboot_id 5d15f771…, benche812b261…), cache tiers enforced and labeled withjuicefs warmup --evict/--check. (Harness lives in fal's setup-time comparison tooling, not this repo — methodology: scenarios below, cache tiers enforced+verified per-scenario, page-cache eviction via fadvise, mount-cache eviction viajuicefs warmup --evict.)--checkbefore load)Reading it as an app would experience it (setup = 10 s of imports + the pack load):
warm_up_directory) does when it isn't overlapped or gated; the integrated API makes it structurally impossible (residency no-op + settle).eager_cpupath: the seeding harness itself first tried to checksum the 24 GB pack via the default lazy mmap walk and blew a 55-minute budget; witheager_cpu=Truethe identical read took 38 s (>85×). That is thecapture_original_weightsshape.Real-app validation (post-scenario-bench)
End-to-end A/B on H100 runners where the ONLY difference is one
prefetch_flashpack_file()line before real cold imports (torch+transformers+diffusers, fresh interpreter per sample), pack state--evict+fadvise-verified 0% before every run:SC_AVPHYS_PAGESguard bug (fixed here — on production nodes the page cache keeps free-RAM ≈ 0, silently no-oping every warm). Post-fix: prefetch at parity-to-winning (loads 26.4–33.1 s vs 33.7–36.8 s control).flashpack==0.4.0control vs this PR, real pack, cold venvs, evict-verified): totals 111.4–122.0 s → 64.1 s (~-45 %); loads 56.8–58.1 s → 27.9 s from a 2-thread warm that reached only ~20 % before the settle cancelled and banked it. Imports were not degraded by the gentle warm.FLASHPACK_PREFETCH_THREADS): measured, 2 threads deliver ≥ the load benefit of 8 with far less collateral I/O.Tests
tests/test_prefetch.py— 22 hermetic CI tests + 1 gpu-marked: bit-identity after prefetch, settle policy (cancel-barely-started / wait-nearly-resident / residency-vs-bytes-read / bounded wait-timeout / noop), in-flight end-to-end cancel through a real eager load, prefetch-vs-load race, concurrent prefetch idempotency, fork-child settle, memory guard, symlink aliasing, error-is-advisory, hot-file no-op,eager_cpuplumbing incl. kill-switch downgrade, eager/lazy revert equality.Registry follow-ups this unlocks (separate PRs)
qwen_image_2512.pydeletes_warm_file_parallel_chunked/_start_weights_page_cache_warm(vendored cattensors) →prefetch_flashpack_file(transformer_pack)at setup start +eager_cpu=Trueincapture_original_weights.wan_vace/edittodrop their git-pinned cattensors ("flashpack-read patch") in favor of the native prefetch.🤖 Generated with Claude Code
https://claude.ai/code/session_016ZBPf2mGKiK6P6WjN785SK