Studio: size Xet download buffers from free RAM, not total - #9052
Conversation
hf_xet's reconstruction buffers are sized from the machine's TOTAL RAM, so a download started while a model is loaded asks for the same multi-GB budget it would ask for on an idle box. Those buffers are the worker's RSS, not reclaimable page cache, so the request and the resident weights add up and the machine swaps. Clamp the zoo-sized budget to a quarter of free RAM by re-asking the zoo about a smaller machine, so buffer, per-file and file count scale together. A quarter of AVAILABLE always exceeds the zoo's eighth of TOTAL on an idle host, so the clamp is unreachable unless RAM is genuinely held: sizing is byte-identical when there is headroom. Also apply the zoo's existing MIN_XET_RAM_BYTES floor to free RAM when picking a transport, so a host too tight for even the clamped floor downloads over HTTP. Fixes #9032
for more information, see https://pre-commit.ci
The recompute calls xet_env_overrides directly, without the throttled flag apply_xet_env threads through after a 429, so an un-throttled recompute could hand back a stream ceiling that backoff had lowered. Take the smaller of the two per key. Every derived value is monotonic in total RAM, so the result stays coherent.
for more information, see https://pre-commit.ci
The Studio UI never sends transport_mode=auto. effectiveTransportMode() resolves Auto through get_download_transport_capabilities(probe=true) and submits the answer as an explicit xet/http, which resolve_requested_use_xet honours without calling resolve_auto_use_xet. The gate therefore never ran on the primary flow. Move the verdict into a shared free_ram_pressure_reason() and call it from the probe as well, so the UI path and an API caller that sends auto agree. Probe only, so an ordinary browse poll stays read-only and still does not load Zoo.
for more information, see https://pre-commit.ci
Both early returns in resolve_auto_use_xet skipped the gate: a zoo without hf_xet_health, and a health probe that raises. Free RAM is read from hf_xet_tuning, a different module, so neither says anything about whether the machine can afford Xet right now. Fold both into one optimistic path that still consults free RAM, and move the registry probe's RAM read outside the health try for the same reason. A health verdict that already demoted keeps its own reason.
for more information, see https://pre-commit.ci
A worker allocates inside the child, after Popen returns, so free RAM does not move until well after sizing. Four downloads starting together each read the same untouched value and each took a quarter of it, promising the whole machine. Sizing now subtracts what live siblings were already promised, and the reservation is bound to the worker's pid so it frees when that worker exits. The transport gate subtracts it too: the clamp bottoms out at Xet's floor, so enough simultaneous workers would still add up past free RAM, and the next download is better served by HTTP than by Xet at its minimum.
for more information, see https://pre-commit.ci
The ledger read and the reserve sat in separate critical sections, so concurrent sizings could all read the same total before any of them wrote, which is the overcommit the ledger exists to stop. Hold the lock across the whole decide-and-reserve region; the recompute inside is pure arithmetic on a frozen profile, and the RAM/disk reading stays outside it. The earlier reservation tests started workers sequentially, which never entered that window. The new test races four threads through it.
…FORCE_XET override the free-RAM gate
Review summaryVerdict: useful, merge it. It fixes a real, deterministic defect, and the scoping is honest. Before / afterBefore, After, the zoo still decides and Studio just hands it a smaller machine. Measured against the real zoo: Does it break anythingNo, and the strong form of that holds: this PR can never refuse a download. Old installs: a 12-combination missing-attribute matrix (no Two fixes pushed (b132624)1. Windows liveness probe (P0, data corruption). 2. All four new tests fail on unfixed source and pass with the fix; 77 passed, 2 skipped; ruff clean. Simulation428 new simulations plus 109 repo tests, all passing. Platform x accelerator matrix run over all 24 cells of [Windows, Linux, WSL, macOS] x [NVIDIA, AMD, CPU-only, no torch] - simulated at the Limits worth stating rather than hiding
CIThe one red check, |
The probe answer was cached for 30 seconds, so several Auto downloads begun inside that window were all admitted on the same pre-reservation verdict and each submitted an explicit transport_mode=xet, which the start path honours without re-reading free RAM. Measured on a 32 GB box with 5 GB free: four starts promised 4.25 GB of Xet buffer pools, one per worker process, where a live gate admits the first and sends the rest over HTTP. Render polls still answer from the cache; only the probe itself has to be live, and it costs no network probe because the health verdict is memoized upstream.
The ledger charged a bound worker's whole buffer promise for its whole lifetime, but the free-RAM reading it is subtracted from has already dropped by whatever that worker allocated, so the same bytes went out twice. Measured with a real child holding real touched RSS: 4.04 GB genuinely free, reported as 2.0 GB, and the next Auto download demoted to HTTP for room that existed. The reservation exists to bridge the window between sizing and allocation, which is what the comment above it says, so credit what has materialised and keep the full promise for a worker that has not bound yet. The xet buffer is drawn through a permit semaphore rather than preallocated, which is why resident memory is the right measure of how much of the promise is already spent.
for more information, see https://pre-commit.ci
|
@codex review |
|
@codex review |
|
@codex review |
|
@codex review |
|
Confirmed the sizing in unsloth_zoo/hf_xet_tuning.py reads total_ram_bytes only and available_ram_bytes is unused, so the clamp is addressing a real defect. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc30a1efa5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| pressure = _memory_pressure_reason() | ||
| if pressure is not None: | ||
| return (False, pressure) |
There was a problem hiding this comment.
Recheck RAM when admitting each Xet worker
When multiple Auto downloads start concurrently, every request can pass this pressure check before any request reaches spawn_worker and records a reservation; model starts widen that window further by awaiting GGUF metadata resolution before spawning. All requests then retain use_xet=True, while the later clamp bottoms out at _MIN_BUFFER_LIMIT, so enough concurrently admitted workers can collectively reserve more RAM than is available and recreate the host-lockup condition this change targets. Make the pressure decision and reservation atomic at worker admission, or recheck immediately before each spawn.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The ledger already closes the case that actually happens, which is one download starting while others run: free_ram_pressure_reason reads the reservations, so the second request sees the first. What is left needs several Auto downloads issued inside the same metadata-resolution window on a host sitting right on the 4GB threshold. Rechecking inside spawn_worker cannot fix that either, since the registry row, the UI and the partial-file writer are already committed to xet by then and a late downgrade would break resume rather than protect RAM.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 468bf1412c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if now - stamp > _BOUND_RESERVATION_TTL or not _pid_alive(pid): | ||
| _budget_reservations.pop(token, None) | ||
| continue | ||
| total += max(0, nbytes - _worker_rss(pid)) |
There was a problem hiding this comment.
Credit only Xet buffers against the reservation
When concurrent Xet workers are live, _worker_rss(pid) includes the Python interpreter, imported Hugging Face libraries, metadata, and other allocations—not just reconstruction buffers. Because that unrelated RSS is already absent from available_ram_bytes, subtracting it from nbytes effectively ignores it entirely: for baseline RSS B and promised buffer P, the code reports (available - B) - (P - B) = available - P rather than available - B - P. Multiple workers can therefore appear to have more unclaimed RAM than they actually do and be admitted with buffer promises that recreate the host-overcommit condition this ledger is intended to prevent; track a buffer-attributable delta or otherwise preserve the non-buffer portion of each worker's reservation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The buffer-attributable delta is not observable here. bind_worker_budget runs in the finally block immediately after Popen, so the only baseline we could snapshot is a freshly exec'd interpreter of a few MB, and a delta from that would count the worker's own huggingface_hub and hf_xet imports as drawn buffer, landing back on today's number. At steady state, when the promise is fully drawn, the current arithmetic is exact; the optimism is one interpreter baseline per worker during the transient and is already bounded by the cap at the promise, so I would rather keep the simple accounting than add a constant we cannot measure.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 468bf1412c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Reserve user-pinned Xet buffer limits
When HF_XET_RECONSTRUCTION_DOWNLOAD_BUFFER_LIMIT is explicitly set, the zoo's setdefault behavior omits that key from sized, so this early return also skips _reserve_worker_budget. Unlike the earlier general admission race, this fresh bypass persists even after the ledger was added: concurrent workers using a user-pinned limit remain invisible to free_ram_pressure_reason() until their buffers become resident, allowing each worker to allocate the same explicit cap. Preserve the user's value, but reserve the effective limit read from env.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
A pinned HF_XET_RECONSTRUCTION_DOWNLOAD_BUFFER_LIMIT is the same deliberate stand-down as a user-set HF_XET_HIGH_PERFORMANCE: the zoo leaves the value alone, so we neither clamp it nor account for it. Reserving a number we never wrote would only push a sibling download to HTTP, and it cannot prevent the pressure the pin itself creates, since the very first worker already takes the full pinned buffer with no clamp.

Addresses #9032. Deliberately not "Fixes": the defect below is proven and deterministic, but I have not reproduced the reporter's spike on their hardware, so I would rather the issue stay open until they confirm.
Problem
RAM spikes to 2-3x while a model downloads alongside a running model, and the machine locks up.
hf_xet's reconstruction buffers are sized inunsloth_zoo.hf_xet_tuning.xet_env_overrides, and every step of that sizing readstotal_ram_bytes:SystemProfile.available_ram_bytesis computed right beside it and read by nothing. So the budget is identical whether the box is idle or nearly full:Those buffers are the worker's RSS, not reclaimable page cache, so on the reporter's WSL2 VM the download's allocation lands on top of a resident 27B
UD-Q4_K_XLand the two together are the swap. Measured budgets for a single worker with the disk clamp idle:Two things make it worse.
hf_xet_healthappliesMIN_XET_RAM_BYTESto total RAM too, so a 32 GB host with 2 GB free still starts on the heavier transport. And_preflight_disk_spacefails a download that will not fit on disk, but nothing inhub/reads memory at all.Fix
Both changes sit in the Studio layer. The zoo stays the one place that decides how big a download should be, and this only ever hands it a smaller machine to decide about, so the two cannot drift the way Studio's old hand-rolled copy did.
clamp_to_available_raminutils/hf_xet_fallback.pypost-processes what the zoo sized. If the budget exceeds a quarter of free RAM, it re-asksxet_env_overridesabout a proportionally smaller machine so limit, shared buffer, per-file size and concurrent-file count all scale together, and bottoms out at the zoo's own_MIN_BUFFER_LIMIT._memory_pressure_reasoninhub/services/download_lifecycle.pyapplies the zoo's existingMIN_XET_RAM_BYTESthreshold to free RAM. Below the floor the answer is a different transport, not a tinier buffer. It only ever demotes, so a machine the zoo already sent to HTTP keeps the zoo's reason.This does not slow any download down
The clamp is unreachable with headroom by construction: the zoo's budget is an eighth of TOTAL, the clamp triggers at a quarter of AVAILABLE, and total is never below available. Measured on a 68.7 GB host, disk-clamped to 3.60 GB in-flight:
Three more properties keep it free:
HF_XET_HIGH_PERFORMANCEstill stands the caps down. That falls out rather than being special-cased: the zoo drops its cap keys, so no budget key reaches the clamp. The same mechanism protects any explicitly set variable, since the zoo's apply is setdefault and a user value never lands in what it reports writing.Tests
9 new tests across
test_hf_xet_fallback.pyandtest_hub_download_transport_auto.py: no-op with headroom, clamp engages under pressure, floor, high-performance stand-down, user-set keys untouched, every degradation path, the transport gate in all four states, and one test pinning the arithmetic against the real zoo formulas so a future sizing change cannot quietly reintroduce a budget bigger than free RAM.57 pass in those two files, plus 755 download and transport tests and 555 hub tests.
ruff checkclean.Pre-existing failures on my macOS box are unchanged by this branch, each confirmed by stashing and re-running:
test_hf_cache_settings.pyandtest_video_routes.pyfail 8 of 71 identically with and without the patch, andtest_unresumable_partial_purge.pyhas one case-insensitive-filesystem failure either way.What this does and does not claim
Proven: the sizing reads total RAM only,
available_ram_bytesis dead, and the budget is identical at 30x different free RAM. That is a real defect regardless of #9032.Not proven: that it is the whole cause of the reporter's 2-3x spike. I have not reproduced it, and I cannot tell from the report whether they were on the Xet transport at all. If their download ran over HTTP, this changes nothing for them. The reporter can check which transport the download used, and confirm whether the spike is smaller on this branch.
Not covered here
The page-cache half. Writing roughly 18 GB through the cache evicts the running model's mmap'd pages, since llama.cpp gets no
--mlockor--no-mmapunless "reserve system RAM" is on, and WSL2 releases guest page cache lazily. So some apparent spike remains even with buffers clamped. The fix is aposix_fadvise(DONTNEED)sweep trailing the writer in the download worker, which is Linux-only and touches the resume-critical.incompletepath, so it belongs in its own PR.