runtime : add --run-time-repack auto mode for swap-bound MoE safety#1738
runtime : add --run-time-repack auto mode for swap-bound MoE safety#1738AndrewMoryakov wants to merge 4 commits into
--run-time-repack auto mode for swap-bound MoE safety#1738Conversation
|
I guess only part is annoyingly having to add 1 after -rtr |
|
@Ph0rk0z thanks for the early flag — to make sure I'm reading you right: The bare You can verify in the diff: So in practice:
If you meant something different — e.g. you'd like the default of |
Adds a third value (`auto`) to the `--run-time-repack` / `-rtr` CLI flag, along with a matching `-rtra` / `--run-time-repack-auto` alias, that turns the existing repack-on-load behaviour into something safe to leave on by default. Motivation ---------- `-rtr 1` (run-time repack into row-interleaved variants such as `Q4_K_R4`) is a clear win on in-RAM MoE — it activates the AVX-VNNI 256-bit GEMM kernels merged earlier (ikawrakow#1467, ikawrakow#1474, ikawrakow#1482, ikawrakow#1472, ikawrakow#1578), giving a measurable PP boost on Zen4 / Sapphire Rapids+ hardware. On models that overflow physical RAM the same flag is a footgun: the repack pass reads every tensor sequentially, which in turn page-faults swapped tensors back into RAM and evicts others, causing thrashing. On Ryzen 9 7950X (96 GiB RAM) we see TG drop ~50% on a 151 GiB MiniMax quant when `-rtr 1` is on vs off. Users hit this regularly because the flag is a recommended performance knob with no in-product warning. Behaviour --------- `-rtr` now accepts an optional value: -rtr 0 / off disable run-time repack -rtr 1 / on enable, no safety check (legacy) -rtr auto enable, but auto-disable when the model is a MoE that exceeds 90% of physical RAM (probed at load time) The bare `-rtr` form keeps the legacy behaviour (`= 1`). When `auto` decides to disable repack it logs: llama_model_load: --run-time-repack auto: disabled (MoE model 151.0 GiB > 90% of RAM 95.1 GiB) Implementation -------------- * `gpt_params::repack_tensors_auto` and matching field on `llama_model_params` carry the request through to the model loader. * `llama_get_total_ram_bytes()` is a small per-OS helper: - Windows: `GlobalMemoryStatusEx().ullTotalPhys` - Linux: `sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGE_SIZE)` - macOS: `sysctl({CTL_HW, HW_MEMSIZE})` - other: returns 0 → caller treats it as "unknown, do nothing" * `llama_rtr_auto_should_disable()` probes the GGUF metadata-only via a short-lived `llama_model_loader` (mmap, no repack), reads arch and hparams, and returns true only if all of these hold: - `repack_tensors_auto` was actually requested, - GPU offload is *not* in play (`n_gpu_layers <= 0`); see "GPU offload" below, - we successfully read total physical RAM, - the probe identifies the model as a MoE (`n_expert > 0 && n_expert_used > 0`), - on-disk model size > 90% of physical RAM. Any failure path (probe exception, RAM unknown, dense model, fits in RAM) returns false and leaves the user-supplied `-rtr` setting alone. * `llama_model_load` consults `llama_rtr_auto_should_disable()` once before constructing the real loader and flips `params.repack_tensors` off in-place when the policy fires. GPU offload ----------- When the user is doing GPU+CPU split inference (`-ngl > 0`, `-cmoe`/`-ncmoe`, etc.) the on-disk model size is no longer a good proxy for the CPU-side RAM footprint, so the auto policy steps out of the way and lets the user's explicit `-rtr` choice stand. This is the conservative call: we'd rather miss an opportunity to auto-disable than auto-disable a build where the CPU side easily fits. Validation ---------- Tested on Ryzen 9 7950X (96 GiB), MSVC 2022 build: * In-RAM Qwen3-30B Q4_K_M (17 GiB) with `-rtr auto` keeps repack enabled. PP/TG numbers indistinguishable from `-rtr 1` (within r=3 stddev). * MiniMax-M2.5 Variant-A (123.6 GiB) with `-rtr auto` correctly auto-disables and logs the reason. * Qwen3-30B with `-rtr auto -ngl 1` keeps repack enabled (GPU offload skip path). * File-not-found probe falls back to the user's setting and prints a single warning. Linux and macOS paths are code-only (compile-tested) — happy to follow up with a Docker-based smoke test if useful. Threshold rationale ------------------- The 0.9 buffer is a deliberate compromise: too tight (e.g. 0.95) and we miss models that are already swap-bound after kernel/other-process RAM use; too loose (e.g. 0.8) and we flip off rtr on plenty of models that would still have benefited. On Linux/Windows desktops "background overhead" tends to be in the 5–10% range, which is what 0.9 covers. The value is hard-coded for now; happy to expose it via env var if there's demand. Out of scope ------------ This PR does not change the default value of `-rtr` (still `0`), and does not alter the bare `-rtr` form (still `= 1`). Adding `auto` as the new default could be a separate follow-up once the policy has been in the field for a while.
f9e49b2 to
0115ace
Compare
|
Self-review found a real The bug: the parser was unconditionally setting
The fix (commit
End result is the four-way matrix that I think people would expect:
Diff is Sorry for the noise — no changes to the API surface, only the internal coupling between repack and mmap got tightened. |
|
Given that I tested EDIT: For the above, I read your note about GPU+CPU split inference; if the path can't determine whether repacking should be enabled or disabled, isn't the wiser choice to disable? Otherwise, the user is left with the same failure message they would have gotten if they just used Also, checking total system memory is not sufficient because the allocation can still fail if other processes are consuming a significant amount of RAM. In that case, checking available memory would be more relevant. I'm not sure it's necessary, but, if we do want to guard |
|
Thanks for catching this on a real workload. The reproducer is correct, and the current policy is wrong on it. There are two concrete bugs in the code at 0115ace. First, the auto path skips the check whenever n_gpu_layers > 0. The intent was to avoid false positives when GPU offload moves most weights out of CPU RAM, but the check only looks at n_gpu_layers and ignores tensor_buft_overrides. In your command, -ngl 99 offloads the non-expert parts, while -ot exps=CPU moves the expert weights back to CPU. That is still a swap-bound CPU-side load. The early skip returns false, the auto path keeps repack enabled, and llama_model_load then forces use_mmap=false, so it behaves like plain -rtr. Second, the policy uses total physical RAM. That is not the right value for an allocation-safety decision. A busy system, page cache pressure, or container limits can make the effective available memory much lower than MEMORYSTATUSEX::ullTotalPhys or sysconf(_SC_PHYS_PAGES). The check should use available/effective memory instead. I also agree with your edited point about uncertainty. If the auto path cannot determine that repack is safe, keeping repack enabled leaves the user with the same failure mode as -rtr. For an explicit safety mode, unknown should disable repack, preserve mmap, and log a warning. If we keep the explicit -rtr auto design, the focused fix is:
There is still a conservative false-positive risk because the current probe compares full GGUF tensor bytes, not exact CPU-resident bytes after placement. That is safer than the current false negative, but a fully precise solution would need a later placement-aware estimator. On the larger question: making -rtr itself self-protective is reasonable, but it changes legacy behavior and is not just a small patch on top of the current PR. The current legacy parser sets use_mmap=false for -rtr at parse time. If we add a load-time safety check without moving that coupling, we can still end up with repack disabled but mmap also disabled. A self-protective -rtr design should leave mmap untouched in the parser and resolve mmap only after the safety decision. @ikawrakow, which direction would you prefer? (a) keep the explicit -rtr auto mode and apply the focused fix above; If there is no strong preference, I will go with (a) as the smallest reviewable change. |
|
In order this to be useful, it needs to consider actually available RAM and take into account tensor overrides and quantization types (i.e., use only the tensors that will get repacked when computing required memory). Also, if I was a user with a low-RAM system where the model that I want to use does not fit in RAM, but I want to use the repacked quantization types, I would just offline repack the model using This would allow me to use the repacked model with mmap, which would be much better than swapping in and out. |
|
Thanks for the direction. Plan is to walk One thing I want to flag. The loader forces For context: I run a CPU-only Zen4 box with 96 GiB RAM and no GPU. I cannot runtime-validate the |
Addresses the two policy gaps community contributor dmaivel reported on PR ikawrakow#1738 plus the related uncertainty-default question: 1. n_gpu_layers > 0 skip removed. The previous skip was too coarse: on a swap-bound configuration with -ngl 99 -ot exps=CPU the GPU loads only the small non-MoE portion while expert weights stay on CPU, so the model size was still the right proxy for CPU RAM pressure. The early skip caused the auto policy to keep repack enabled and behave identically to plain -rtr 1 on dmaivel's Linux 64 GiB / 120 GiB MoE setup. 2. Total physical RAM replaced by available/effective memory. Repack safety depends on currently-allocatable bytes, not installed RAM. Busy systems and containers can fail an allocation that fits in total RAM. New helper llama_get_available_ram_bytes uses: - Windows: MEMORYSTATUSEX::ullAvailPhys - Linux: /proc/meminfo MemAvailable, falling back to sysconf(_SC_AVPHYS_PAGES), then taking min with cgroup v2 memory.max - memory.current or cgroup v1 memory.limit_in_bytes - memory.usage_in_bytes when running inside a container; the cgroup walker climbs the hierarchy up to 32 levels to honor inherited limits - macOS: host_statistics64 with (free + inactive) * page_size 3. Bool return replaced by an enum decision so unknown is distinguishable from safe. Cases: - KEEP: policy says safe, repack on, mmap forced off (legacy) - DISABLE: policy says unsafe, repack off, mmap left as user/default - NOT_APPLICABLE: dense model, leave both alone, log INFO - UNKNOWN: probe exception or RAM query failure, safety-first disable repack, leave mmap, log WARN with reason Mmap interaction note: the parser fix from the previous commit already leaves use_mmap untouched at parse time for `-rtr auto`. The load-time logic now resolves the final mmap state alongside the repack decision, so DISABLE / UNKNOWN preserve mmap and KEEP forces it off (matching legacy `-rtr 1`). Help text and policy log messages updated from "RAM" to "available memory" so the user sees the actual metric in diagnostics. Validated locally on six model classes: - Qwen3-30B-A3B Q4_K_M (in-RAM MoE): KEEP - gpt-oss-20b MXFP4 (in-RAM MoE): KEEP - gpt-oss-120b MXFP4 multi-shard (in-RAM MoE): KEEP - Qwen3.5-27B Q8_0 (dense): NOT_APPLICABLE - Qwen3.5-397B-A17B UD-Q4_K_XL 6 shards 219 GiB (swap-bound MoE): DISABLE; probe.n_bytes correctly accumulates to 204.2 GiB across all shards - MiniMax-M2.5-TaperedRAM 89.6 GiB (swap-bound near-RAM MoE): DISABLE on a 96 GiB system with ~78 GiB available Bench overhead between v1 and v2 paths in the KEEP/KEEP scenario is statistically zero at r=5 (Qwen3-30B PP +0.5%, TG -1.1%; gpt-oss-20b PP +1.8%, TG -0.08%, all inside σ overlap). The v2 dispatch wrapper adds no measurable inference overhead. Linux cgroup walker is compile-tested only; runtime-tested by the local Windows build and smoke-tested via /proc-style logic but not on an actual Linux container. Reviewers with Linux access are invited to exercise the cgroup-aware path under cgroup v1 and v2.
|
Pushed an update based on the maintainer feedback and dmaivel's reproducer. Current head is What changed since
I also updated the PR description because the original body still described the old total-RAM / GPU-skip heuristic. Local validation: MSVC 2022 |
Three small cleanups that address reviewer findings on the previous placement-aware commit: 1. Drop the dense `if (!is_moe) return NOT_APPLICABLE` fast-exit. Dense models with -rtr auto now go through the same byte-count walk as MoE. If the dense quant has repackable types and fits, the policy keeps repack; if it has none, the policy disables repack instead of leaving repack=true and silently forcing mmap=false in the loader. Dense Q8_0 fits in available memory on the typical workstation and benefits from Q8_0 -> Q8_0_R8 repack; hypothetical large dense F16 without AVX512BF16 no longer slips past the auto policy unprotected. 2. Move unsupported-mode UNKNOWN exits (--fit, merge_qkv, merge_up_gate_exps, split_mode ATTN/GRAPH, avail-RAM query, and regex compile) before the metadata probe. The probe is a real I/O cost on multi-shard models; bailing out early avoids paying it just to return UNKNOWN. 3. Remove the redundant `if (cpu_repackable_bytes <= threshold)` wrapper around the KEEP path and the unreachable trailing UNKNOWN. After the two upstream `>` checks, the condition is always true; the dead trailing return signaled doubt rather than design. Also drops `NOT_APPLICABLE` from the decision enum and its caller-side case (now unreachable). The decision is a clean tri-state: KEEP / DISABLE / UNKNOWN. Header comment updated to match. Smoke matrix on CPU-only Zen4 96 GiB, all green: Qwen3-30B Q4_K_M (17 GiB) -> KEEP Qwen3.5-27B Q8_0 dense (29 GiB) -> KEEP (was NOT_APPLICABLE) MiniMax M2.5 (90 GiB) -> DISABLE (total-bytes gate) Qwen3-30B + --merge-qkv -> UNKNOWN (early exit, no probe) Qwen3-30B + -ot 'attn.*=CPU' -> KEEP, override placement OK gpt-oss-20b MXFP4 (11 GiB) -> KEEP, repackable 1.2 GiB only No GPU here so the -ngl 99 -ot exps=CPU reproducer still cannot be runtime-validated locally; the override placement logic itself was exercised on a CPU-only -ot setup above.
|
Pushed a small follow-up cleanup at Three changes addressing reviewer feedback on
Also dropped Smoke matrix on CPU-only Zen4 96 GiB, all green:
The |
What
Adds a third value (
auto) to--run-time-repack/-rtr, plus an-rtra/--run-time-repack-autoalias.-rtr autoenables run-time repack only when the metadata-only safety policy can determine that it is safe for the current load. If the policy determines that repack would likely turn a mmap-friendly swap-bound MoE load into an anonymous-memory load, it disables repack and leaves the user/default mmap setting intact.The bare
-rtrform is unchanged and still means legacy-rtr 1.Behaviour
0/off1/onautoThe default when
-rtris absent is unchanged (0).Policy
The auto policy now follows maintainer feedback from the PR discussion:
iqk_repacked_type();The policy probes GGUF metadata only, walks
probe.weights, resolves simple CPU/GPU placement, and compares against90%of currently available memory.Placement resolution is intentionally bounded:
tensor_buft_overridesare applied first, using the samestd::regex_search/ first-match semantics as the loader;LLAMA_SPLIT_MODE_LAYER/LLAMA_SPLIT_MODE_NONEplacement is modeled fromn_gpu_layers, output-layer rules, and already-populatedmodel.devices;-ncmoeis handled for known simple cases, including all/first-N expert CPU overrides and single-device layer split;--fit, merged tensor modes, graph/attention split modes, and multi-device-ncmoelayer distribution are treated as unknown and therefore safety-disable repack.Memory Gates
Primary gate:
CPU-resident repackable bytes > 90% available memory=> disable repack.Secondary gate:
total CPU-resident tensor bytes > 90% available memory=> disable repack.The secondary gate exists because the current loader still forces
use_mmap=falsewheneverrepack_tensors=true; even if only a subset of tensors is actually repackable, keeping repack enabled can make all CPU-resident tensors lose mmap.Unknown also disables repack. This is intentional for explicit
-rtr auto: a safety mode should not fall back to the same failure mode as plain-rtr 1.Available Memory Helper
MEMORYSTATUSEX::ullAvailPhys/proc/meminfoMemAvailable, fallback to_SC_AVPHYS_PAGES, capped by cgroup v2/v1 headroom when presenthost_statistics64using(free_count + inactive_count) * page_sizeMmap Caveat
This PR fixes the
repack_tensors=true => use_mmap=falsepath for-rtr auto, but it does not fully decouple repack from mmap internally.There are still independent loader paths that can disable mmap. In particular, CPU tensor overrides can disable mmap unless
GGML_CUDA_NO_PINNED=1is set. The auto policy now logs a warning for that case when it disables repack.A full design where only repackable CPU tensors are copied to writable memory while non-repackable CPU tensors remain mmap-backed is larger loader work and intentionally out of scope here.
Validation
Current pushed head:
1441ee9a9.Validated locally on Windows / MSVC 2022:
nmake llama-clisucceeds;llama-cli --helpshows-rtr, --run-time-repack [0|1|auto].I do not have a local GPU machine, so the exact
-ngl 99 -ot exps=CPUruntime reported by dmaivel is not runtime-validated here. The code path is covered by the placement resolver through the manual tensor override handling.Out of Scope
-rtr.-rtrself-protective.