feat(moe): partial-pin serving to beat the Windows/WSL2 pinned-memory quota (--pin-exempt-layers) - #27
Conversation
…layers) Windows/WDDM caps GPU-registrable host memory at ~50% of physical RAM (WSL2 included; measured 94 GiB on a 192 GB machine, independent of the WSL VM size). Models whose expert banks exceed the quota currently fail at load with cudaHostRegister errors; the documented fallback is the pure-CPU MoE backend. This adds a middle path: pin only as many layers as the quota allows. Exempt layers stay pageable, decode on the existing CPU executor (they are auto-unioned into cpu_layer_ids), and prefill through the legacy full-layer copy, which the driver stages from pageable memory. The offload cache's fused copy plan and hit-D2D prefetch skip exempt layers; set_bank_sources validates that every non-pinned layer is CPU-decoded and that prefill overlap is enabled. --pin-exempt-layers accepts the same grammar as --moe-cpu-layers (explicit ids, a count strided evenly across depth, or a fraction). v1 wires the nvfp4 provider; other providers, model-owned setups, FTW checkpoints, and dummy weights refuse loudly instead of silently pinning everything. No flag means byte-identical behavior to today. Measured on an RTX 5090 (32 GB) + 192 GB Windows/WSL2 box: MiniMax-M2.5 (229B, 126 GB of NVFP4 banks, previously unservable) loads in 127 s with 47/62 layers pinned (89.4 GiB) and decodes at 9.1-9.6 tok/s; a 29k-token prompt prefills in 35 s cold, 7 s on a radix repeat.
|
Triaged while assembling a merged deployment branch for a 2× RTX 6000 Ada / 2× Xeon Gold 6526Y Linux box serving DeepSeek-V4-Flash with offloaded experts, to benchmark the open PRs together. Partial-pin serving for Windows/WSL2. Not merged — Linux here, with memlock raised to unlimited, so the constrained-pinning path this targets never triggers. Worth noting from the Linux side that the same symptom has a different cause there: the default RLIMIT_MEMLOCK of RAM/8 (62 GiB of 503) fails a 155 GiB registration partway through a load. If partial-pin has a Linux-visible path it might serve that case too, but I have not tested it. Flagging only so the absence of a report from me is not read as a problem found — I merged and benchmarked #30, #48, #56, #69, #70, #71 and #81, and left this one out deliberately. |
|
Native-Windows data point supporting this, plus what looks like a help-text/code SetupWindows 11 (native, not WSL2), RTX 4090 24 GB, 63 GB RAM, driver 610.88, Observations
Quota sanity check between runs: a single fresh-process Post-reboot data (which narrows the diagnosis)After a reboot cleared the phantom VRAM:
Net: on a 63 GB Windows box, this model's usable KV is capped around 8-12k Interpretation: pinning the KV pool evicts expert cache from VRAM into host Help-text vs code
Happy to test the PR branch on this native-Windows box if useful — it seems to |
|
We've implemented this in #112 — --moe-cpu-layers now keeps the selected layers' banks in OS-locked memory instead of pinned. Closing this in favor of that. |
|
Correction to one claim in my comment above, in the interest of accuracy. We have now identified what actually held memory across our failed runs: an Consequence for my earlier analysis: the "Gemma at Refined practical guidance for anyone hitting
|
Problem. Windows caps cudaHostRegister-able host memory at roughly 50% of physical RAM (WDDM-managed; WSL2 inherits it and NVIDIA marks it not actionable:
see the long-standing threads on the NVIDIA forums, e.g. "Change limit of 50% for cudaHostAlloc pinned memory on Windows 10/11"). FreeToken pins every expert bank, so on any Windows/WSL2 machine the servable-model ceiling is the pin quota, not
RAM. We measured the quota at 94 GiB on a 192 GB box and confirmed it is independent of the WSL VM size (identical at 100 GB and 160 GB .wslconfig). MiniMax-M2.5's ~126 GB of banks cannot load at any setting; the paper's fallback
for unpinnable pools is the all-CPU backend.
This PR adds the middle path the code already anticipates.
HostResidencyhas PAGEABLE,ExpertBanks.layer_residencyandset_bank_sources(..., layer_residency=...)are already plumbed, andoffload_cache.pyraises NotImplementedError for non-pinned layers. This change implements that stub:--pin-exempt-layers(same grammar as--moe-cpu-layers: ids, count evenly strided, or fraction). Exempt layers skip pinning at load (the per-layerPinPipelinesink honors an exempt set) and are auto-unioned intocpu_layer_ids, so their decode uses the existing CPU executor unchanged..copy_()into the overlap double buffer; from pageable memory the driver stages it (slower, and the enqueuing thread stalls for the staging, but correct under the existing ready/release event discipline). The hit-D2D split prefetch and the fused copy plan skip exempt layers;set_bank_sourcesvalidates exempt is a subset ofcpu_layer_idsand thatmoe_prefill_overlapis on.setup_offload_expert_banksmodels, FTW checkpoints, and--use-dummy-weightraise NotImplementedError with clear messages rather than silently pinning everything. With the flag unset, behavior is byte-identical to main.Numbers (RTX 5090 32 GB, 192 GB DDR5, Windows 11 + WSL2, MiniMax-M2.5-NVFP4, 229B, 62 MoE layers at 1.90 GiB of banks each):
--pin-exempt-layers 15pins 47 layers (89.4 GiB, under the 94 GiB quota) and the model serves: READY in127 s (129 GB expert read at 2.5 GB/s, parallel loader), decode 9.1-9.6 tok/s across context lengths, 28.9k-token prompt TTFT 34.8 s cold / 7.1 s on radix repeat. Without this change the same command dies at cudaHostRegister ~117 s in.
Tests.
tests/moe/test_pin_policy.py(grammar, 11 cases) andtests/moe/test_residency_propagation.py(the set_bank_sources contract, 9 cases) run CPU-only. The existing CUDA suites most affected (test_offload, test_fused_copy, test_prefill_hit_d2d, test_hybrid_fetch: 29 tests) pass unchanged on all-pinned defaults.Not in this PR, open to direction: an
autofit-to-quota policy (needs a cached quota measurement; we prototyped a probe and would follow up), a pinned staging ring to restore fully-async prefill for exempt layers, and extending the plan to the other providers. Happy to adjust naming, split the change, or add coverage wherever you want it.