Skip to content

runtime : add --run-time-repack auto mode for swap-bound MoE safety#1738

Open
AndrewMoryakov wants to merge 13 commits into
ikawrakow:mainfrom
AndrewMoryakov:pr/rtr-auto-mode
Open

runtime : add --run-time-repack auto mode for swap-bound MoE safety#1738
AndrewMoryakov wants to merge 13 commits into
ikawrakow:mainfrom
AndrewMoryakov:pr/rtr-auto-mode

Conversation

@AndrewMoryakov

@AndrewMoryakov AndrewMoryakov commented May 4, 2026

Copy link
Copy Markdown
Contributor

What

Adds auto to --run-time-repack / -rtr and documents the -rtra / --run-time-repack-auto alias.

Mode Behaviour
0 / off Disable run-time repack
1 / on / bare -rtr Force legacy run-time repack
auto Repack only when the loader can conservatively prove that the non-mmap load fits; uncertainty disables repack and preserves requested mmap

The default remains 0.

Safety contract

This revision turns RTR auto into a fail-closed loader policy rather than a parser-only heuristic:

  • forced RTR is coupled to effective loader mmap (repack_tensors=true forces loader mmap off), so the repack pass actually executes;
  • auto uses metadata-only placement analysis and checked peak-memory arithmetic before constructing the real loader;
  • every unsupported/ambiguous placement, overflow, memory-accounting failure, incompatible prefetch/defer mode, or platform uncertainty produces AUTO_UNKNOWN, disables repack, and preserves the requested mmap setting;
  • peak accounting includes CPU-resident tensor bytes, maximum repack workspace, loader worker read buffers, and active CUDA staging buffers;
  • tensor buffer overrides use the loader's first-match regex semantics;
  • simple single-device GPU and -ncmoe placement is modeled; multi-device/merged/fit and other ambiguous paths remain UNKNOWN rather than optimistic;
  • Linux memory headroom resolves /proc/self/cgroup and /proc/self/mountinfo, cgroup v1/v2 namespaces, bind/full mounts, ancestors, and v1 memsw, intersecting all applicable limits;
  • cgroup errors remain fail-closed. For v2 only, a missing memory.max + memory.current pair is skipped as unbounded exactly at the resolved mountpoint; a partial pair or missing pair below it is UNKNOWN;
  • Windows Job Object membership is conservatively UNKNOWN instead of using unsafe host-wide headroom; macOS/unsupported platform fallbacks are also conservative.

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-bench writes an immutable test_v3 schema with requested/effective mmap and RTR fields. compare-llama-bench.py reads compatible test, test_v2, and test_v3 sources 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_params structure (repack_tensors_auto and prefetch_experts). Applications linking the library must rebuild against matching headers; mixing old binaries/aggregate initializers with the new library is unsupported.

Validation for head 843de95f

Executed locally:

  • Windows MSVC/Ninja: llama-bench, test-rtr-params, test-cgroup-resolver, test-rtr-auto-peak built successfully;
  • CTest RTR/cgroup suite: 3/3 passed;
  • compare-script Python suite: 14/14 passed;
  • py_compile and git diff --check: passed;
  • Linux/WSL g++ 13.3 with -std=c++17 -Wall -Wextra -Werror: cgroup resolver and peak-accounting tests passed;
  • live cgroup-v2 smoke reproduced the root-mount missing-pair topology and resolved it correctly;
  • real forced-RTR GGUF smoke verified: requested mmap=true, effective loader mmap=false, no mmap-backed model buffers, repack pass executed, n_repacked > 0, status=enabled;
  • Windows auto smoke verified conservative auto_unknown under a Job Object while preserving mmap;
  • real 60-column test_v3 SQL 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

  • changing the default RTR mode;
  • making forced legacy -rtr 1 memory-safe (use auto for that policy);
  • selective per-tensor mmap/repack decoupling;
  • exact Windows Job Object limit accounting;
  • changing malformed-present benchmark schemas from fail-hard to skip-with-warning.

Design notes

The safety contract, implementation stages, and regression matrix are recorded in:

  • docs/RTR_AUTO_PR_FOLLOWUP_SPEC.md
  • docs/RTR_AUTO_PR_FOLLOWUP_PLAN.md

@Ph0rk0z

Ph0rk0z commented May 4, 2026

Copy link
Copy Markdown

I guess only part is annoyingly having to add 1 after -rtr

@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

@Ph0rk0z thanks for the early flag — to make sure I'm reading you right:

The bare -rtr form (no value after it) is preserved in this PR — it still means -rtr 1 exactly like before, so existing scripts and READMEs do not need to change. The optional value is only consumed when the next argv token is one of 0/1/off/on/auto; anything else (or end-of-args) leaves params.repack_tensors = true, params.repack_tensors_auto = false, which is the legacy fast path.

You can verify in the diff: common/common.cpp — the parser only does ++i to consume the value when is_mode_token is true.

So in practice:

Command Behaviour
-rtr legacy on (= 1)
-rtr 0 / -rtr off off
-rtr 1 / -rtr on on
-rtr auto new safety-net mode
-rtra / --run-time-repack-auto shortcut for -rtr auto

If you meant something different — e.g. you'd like the default of -rtr to be auto rather than 0, or you'd like a different short-form spelling — happy to adjust. The default (when -rtr is absent entirely) is unchanged on purpose in this PR; making auto the new default would be a worthwhile follow-up once the policy has been in the field for a bit.

@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

Self-review found a real use_mmap regression — force-pushed a fix.

The bug: the parser was unconditionally setting params.use_mmap = false before parsing the optional value, which leaks two ways:

  1. -rtr 0 (explicit disable) was also disabling mmap as a side effect — surprising because the user said "no repack".
  2. -rtr auto was setting use_mmap = false at parse time, but if the auto policy then disabled repack at load (the whole point of the safety net on swap-bound models!), the load would still proceed with mmap = false — which on a model that already overflows RAM is exactly the wrong thing.

The fix (commit 0115ace21):

  • common/common.cpp: rework the -rtr parser to determine the final state first, and only force use_mmap = false when repack is actually going to run unconditionally (legacy -rtr / -rtr 1). For -rtr 0 and -rtr auto we now leave params.use_mmap alone.
  • src/llama.cpp: in llama_model_load, after the auto policy decides:
    • If auto keeps repack enabled → set use_mmap = false to match the legacy -rtr 1 coupling (repack pass writes back into tensor buffers).
    • If auto disables repack → leave use_mmap as the user configured it, so swap-bound MoE actually loads via mmap as intended.

End result is the four-way matrix that I think people would expect:

Command repack use_mmap (after load)
(no -rtr) off default true
-rtr 0 off default true (was: false 🐛)
-rtr 1 on false (legacy)
-rtr auto (in-RAM) on (auto kept it) false (matches -rtr 1)
-rtr auto (swap-bound) off (auto disabled it) default true (was: false 🐛)

Diff is +24 / -11 on top of the original commit; rebuilt cleanly on MSVC 2022 (Zen4); smoke-tested both the in-RAM and swap-bound paths via llama-bench/llama-cli.

Sorry for the noise — no changes to the API surface, only the internal coupling between repack and mmap got tightened.

@dmaivel

dmaivel commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Given that -rtr requires mmap to be disabled, I’m not sure users running swap-bounded MoE models are likely to specify it in the first place.

I tested -rtr auto with a model that does not fit in RAM, and it behaved the same as -rtr on, which seems problematic. This was on a Linux system with 64 GB of RAM and a ~120 GB model:

GGML_CUDA_NO_PINNED=1 ./llama-server \
    --model ~/.../Qwen3.5-397B-A17B-IQ2_XS-00001-of-00004.gguf \
    -c 4096 -ngl 99 -ot exps=CPU \
    -b 4096 -ub 4096 \
    -rtr auto

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 -rtr in its current form.

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 -rtr, would it make more sense to check whether there is enough available RAM and disable it automatically when there is not? That would avoid changing the argument to require on/off/auto; instead, we could simply print a message saying that rtr was disabled due to insufficient memory.

@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

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:

  1. use available/effective memory instead of total RAM;
  2. remove the n_gpu_layers > 0 skip;
  3. distinguish keep / disable / unknown instead of returning a bool;
  4. make unknown safety-first for -rtr auto;
  5. update the log/help text to say available memory.

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;
(b) rewrite this so existing -rtr becomes self-protective;
(c) defer this to a larger placement-aware design based on estimated CPU-resident bytes.

If there is no strong preference, I will go with (a) as the smallest reviewable change.

@ikawrakow

Copy link
Copy Markdown
Owner

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

./bin/llama-quantize --repack $model_in $model_out type

This would allow me to use the repacked model with mmap, which would be much better than swapping in and out.

@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

Thanks for the direction. Plan is to walk probe.weights, filter by iqk_repacked_type() and resolve placement against tensor_buft_overrides / n_gpu_layers / -ncmoe, then compare CPU-resident repackable bytes against available memory.

One thing I want to flag. The loader forces use_mmap = false whenever repack_tensors=true (src/llama-model-loader.cpp:585), so a KEEP based on repackable bytes alone can still trigger swap when total CPU-resident bytes exceed available memory. I plan to AND-check total CPU-resident bytes as a secondary gate against the same threshold; the repackable-bytes metric stays primary and matches your guidance. Say so if you'd rather drop the total check or decouple repack from mmap separately.

For context: I run a CPU-only Zen4 box with 96 GiB RAM and no GPU. I cannot runtime-validate the -ngl 99 -ot exps=CPU configuration dmaivel reported, since exercising that path needs an actual GPU. The placement resolver handles it in code based on tensor_buft_overrides regex matching, but I have no way to run the case locally. Smoke covers in-RAM, swap-bound, and multi-shard cases on CPU only.

AndrewMoryakov added a commit to AndrewMoryakov/ik_llama-pr that referenced this pull request May 17, 2026
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.
@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

Pushed an update based on the maintainer feedback and dmaivel's reproducer. Current head is 1441ee9a9.

What changed since 0115ace21:

  • the policy now uses available/effective memory instead of installed RAM;
  • the decision is tri-state-ish: unsafe disables repack, unknown also disables repack with a WARN;
  • the check walks probe.weights and filters by iqk_repacked_type() so the primary gate is CPU-resident repackable bytes, not full GGUF size;
  • manual tensor_buft_overrides are applied with loader-compatible regex first-match semantics, so -ngl 99 -ot exps=CPU is no longer skipped just because n_gpu_layers > 0;
  • simple layer placement is modeled for LAYER / NONE split modes; complex placement modes (--fit, merge modes, graph/attention split, multi-device -ncmoe distribution) are treated as unknown and safety-disable repack;
  • there is also a secondary total CPU-resident bytes gate because the current loader still forces use_mmap=false whenever repack_tensors=true.

I also updated the PR description because the original body still described the old total-RAM / GPU-skip heuristic.

Local validation: MSVC 2022 nmake llama-cli succeeds, and llama-cli --help shows the updated -rtr [0|1|auto] syntax. I still cannot runtime-test the exact GPU reproducer locally; that path is covered by the override placement logic, not by hardware validation on my side.

@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

Pushed a small follow-up cleanup at 15e04decd.

Three changes addressing reviewer feedback on 1441ee9a9:

  1. Dropped the dense if (!is_moe) return NOT_APPLICABLE fast-exit. Dense models 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. Closes the swap-thrash edge case on large dense models that lack repackable types in the current build.

  2. Moved the unsupported-mode UNKNOWN exits (--fit, --merge-qkv, --merge-up-gate-experts, split_mode ATTN/GRAPH, available-memory query, and regex compile) before the metadata probe. The probe is real I/O on multi-shard models; bailing out early avoids paying it just to return UNKNOWN.

  3. Removed a redundant if (cpu_repackable_bytes <= threshold) wrapper around the KEEP return and the unreachable trailing UNKNOWN. After the two upstream > checks the condition is always true.

Also dropped NOT_APPLICABLE from the decision enum and its caller-side case (now unreachable). The decision is a clean tri-state: KEEP / DISABLE / UNKNOWN.

Smoke matrix on CPU-only Zen4 96 GiB, all green:

Setup Decision
Qwen3-30B Q4_K_M (17 GiB in-RAM) KEEP
Qwen3.5-27B Q8_0 dense (29 GiB) KEEP (was NOT_APPLICABLE before)
MiniMax M2.5 (90 GiB) DISABLE via total-bytes gate
Qwen3-30B + --merge-qkv UNKNOWN, early exit (no probe)
Qwen3-30B + -ot 'attn.*=CPU' KEEP, override placement applied
gpt-oss-20b MXFP4 (11 GiB) KEEP, repackable 1.2 GiB only

The -ngl 99 -ot exps=CPU reproducer still cannot be runtime-validated on my CPU-only box; the override placement logic was exercised in the -ot 'attn.*=CPU' smoke above.

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.
@AndrewMoryakov

Copy link
Copy Markdown
Contributor Author

Final pre-PR validation — 843de95f

I completed one more full review pass before updating this PR, including an independent five-phase Claude Code review through Maestro Duet. The final whole-PR verdict is GO: no known blocker remains in the reviewed RTR paths.

What the executed checks guarantee

For the configurations actually exercised:

  • forced -rtr reaches the real repack pass: requested mmap stays observable, effective loader mmap is disabled, model buffers are not mmap-backed, and n_repacked > 0;
  • -rtr auto remains fail-closed: every unresolved placement/memory/platform case disables repack and preserves requested mmap;
  • cgroup v1/v2 parsing, mount/namespace resolution, ancestor intersection, and missing-v2-root-pair handling pass their regression tests;
  • checked peak-memory arithmetic rejects overflow;
  • requested/effective mmap and RTR getters are null-safe and distinguish loader decision, mmap-backed buffers, executed repack, count, and final status;
  • test_v3 writer output imports into SQLite, and the compare script keeps schema generations isolated while handling legacy NULL RTR metadata.

Evidence

  • MSVC/Ninja targets built: llama-bench, test-rtr-params, test-cgroup-resolver, test-rtr-auto-peak;
  • CTest RTR/cgroup: 3/3 passed;
  • Python compare suite: 14/14 passed;
  • Linux/WSL g++ 13.3 -Wall -Wextra -Werror header tests: passed;
  • live cgroup-v2 smoke: child memory pair present, root pair absent at mountpoint, correctly treated as an unbounded root level;
  • real forced-RTR GGUF smoke and Windows Job Object auto smoke: passed with the expected effective states;
  • real 60-column test_v3 SQL round-trip: passed;
  • py_compile and git diff --check: passed.

Explicit boundary

This is not a claim that every backend/platform combination has been executed locally. CUDA, Metal, MinGW, and the full upstream matrix remain CI scope. The policy intentionally returns AUTO_UNKNOWN instead of making an optimistic memory decision when it cannot prove safety.

ABI reminder: llama_model_params gained trailing repack_tensors_auto and prefetch_experts fields; linked clients must rebuild against matching headers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants