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 13 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 |
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 |
|
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 |
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.
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.
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.
15e04de to
843de95
Compare
Final pre-PR validation —
|
What
Adds
autoto--run-time-repack/-rtrand documents the-rtra/--run-time-repack-autoalias.0/off1/on/ bare-rtrautoThe default remains
0.Safety contract
This revision turns RTR auto into a fail-closed loader policy rather than a parser-only heuristic:
repack_tensors=trueforces loader mmap off), so the repack pass actually executes;AUTO_UNKNOWN, disables repack, and preserves the requested mmap setting;-ncmoeplacement is modeled; multi-device/merged/fit and other ambiguous paths remain UNKNOWN rather than optimistic;/proc/self/cgroupand/proc/self/mountinfo, cgroup v1/v2 namespaces, bind/full mounts, ancestors, and v1 memsw, intersecting all applicable limits;memory.max+memory.currentpair is skipped as unbounded exactly at the resolved mountpoint; a partial pair or missing pair below it is UNKNOWN;Effective state and tooling
The public model API exposes requested/effective mmap and RTR state separately, including whether mmap-backed buffers exist, whether the repack pass ran, the number of repacked tensors, and the final RTR status.
llama-benchwrites an immutabletest_v3schema with requested/effective mmap and RTR fields.compare-llama-bench.pyreads compatibletest,test_v2, andtest_v3sources without cross-schema joins/averaging, uses effective mmap for v3 comparison, and renders unknown legacy RTR metadata explicitly.ABI/API note
This PR adds trailing fields to the public
llama_model_paramsstructure (repack_tensors_autoandprefetch_experts). Applications linking the library must rebuild against matching headers; mixing old binaries/aggregate initializers with the new library is unsupported.Validation for head
843de95fExecuted locally:
llama-bench,test-rtr-params,test-cgroup-resolver,test-rtr-auto-peakbuilt successfully;py_compileandgit diff --check: passed;-std=c++17 -Wall -Wextra -Werror: cgroup resolver and peak-accounting tests passed;n_repacked > 0, status=enabled;auto_unknownunder a Job Object while preserving mmap;test_v3SQL output was imported into SQLite and compared successfully.A second independent review was run with Claude Code through Maestro Duet across five phases. Its final whole-PR verdict was GO after the cgroup-v2 root case and carry-forward cleanups were fixed.
These are evidence-backed guarantees for the exercised CPU/MSVC, Linux resolver, and Python/SQLite paths—not a universal platform guarantee. CUDA, Metal, MinGW, and the complete upstream matrix remain for upstream CI.
Scope deliberately left out
-rtr 1memory-safe (useautofor that policy);Design notes
The safety contract, implementation stages, and regression matrix are recorded in:
docs/RTR_AUTO_PR_FOLLOWUP_SPEC.mddocs/RTR_AUTO_PR_FOLLOWUP_PLAN.md