Skip to content

Throwaway: Strix Halo memory repro kit for a 96 GiB graphics carve-out - #10418

Closed
danielhanchen wants to merge 2 commits into
mainfrom
strix-halo/96gib-repro-kit
Closed

danielhanchen wants to merge 2 commits into
mainfrom
strix-halo/96gib-repro-kit

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Throwaway PR. Do not merge. It exists so someone with a Strix Halo box set to a large
graphics carve-out can run one script and send back a directory of JSON.

Why

AMD report two things about Strix Halo, both of which they see at a 96 GiB Variable
Graphics Memory setting:

  1. llama.cpp reads the VGM carve-out as shared iGPU memory, so large models will not load.
  2. ROCm will not load more than 64 GB because of an old HIP DLL in the llama.cpp artifact.

Our Strix Halo CI runner is a 128 GB machine with a 64 GiB carve-out, and at that size
neither claim reproduces:

layer what it reports on the runner
registry HardwareInformation.qwMemorySize (the carve-out) 64.00 GiB
GlobalMemoryStatusEx visible RAM 63.65 GiB
the machine (visible + carve-out) 127.65 GiB
Vulkan raw heaps 37.22 GiB and 74.43 GiB (DEVICE_LOCAL)
ggml Vulkan total 111.65 GiB (the sum of both heaps, exactly)
HIP hipMemGetInfo total 99.74 GiB
largest single hipMalloc that allocated and read back 110.2 GiB
UD-Q4_K_XL (about 77 GiB on device) loads and serves on ROCm and on Vulkan

The summation mechanism is real and confirmed to the byte: ggml adds every heap on an
integrated device, not just the device-local ones. But at a 64 GiB carve-out the sum stays
under the machine, so nothing impossible is claimed and the model loads. Windows subtracts
the carve-out from visible RAM, so as the carve-out grows the heap sum grows while the
physical bound does not: at 96 GiB the same call is expected to report more memory than the
machine physically has, which is what AMD saw. That is why this needs their configuration
and not ours.

Same for the allocation cap. The stock rule is not a flat 64 GiB clamp, it is
max(dedicated_VRAM_heap, 0.75 * shared_GART_heap). On the runner the GART branch wins,
which is where 110.2 GiB comes from. On a larger carve-out the dedicated branch may dominate
instead, and then the interesting arm is the memory report rather than the cap.

What to do

On a Windows Strix Halo box, set the carve-out to 96 GiB (firmware UMA Frame Buffer Size, or
Adrenalin -> Performance -> Tuning -> Variable Graphics Memory), reboot, then:

cd strix_halo_repro
powershell -ExecutionPolicy Bypass -File run.ps1 -OutDir C:\strix_out
python report.py C:\strix_out\*.json

It downloads two stock llama.cpp release builds (ROCm gfx1151 and Vulkan) at a pinned tag,
runs six cells, and writes JSON per cell. Nothing is compiled and nothing is installed.
Budget about 30 minutes plus the model download if you include the model cells.

report.py prints one table of what every layer reports next to the machine's real memory,
and three statements as yes or no:

  • over_report: does ggml claim more memory than the machine physically has
  • sums_heaps: is that number the heaps added together rather than the largest one
  • hip_is_vgm_only: does HIP report only the carve-out

On the 64 GiB runner those come out NO / yes / NO. On a 96 GiB box the first is expected to
flip, and that flip is the bug AMD are describing.

The allocation search is deliberately bounded

Cell 4 stops at 92 percent of physical RAM. AMD's own note with the patched runtime warns
that an allocation near 100 percent of usable RAM can hang the host or trigger a
WIN32K_POWER_WATCHDOG_TIMEOUT bugcheck. The question worth answering is whether an
allocation the stock runtime refused now succeeds, not what the new ceiling is, and a
bounded search answers it without going near that condition. Every success is verified by
writing the buffer and reading it back, not by hipMalloc returning.

What to send back

The whole output directory. It contains the host facts, the raw per-heap Vulkan numbers,
the HIP numbers, the allocation boundary, what Studio's own Vulkan probe returns, and
llama-fit-params on both backends. README.md in the folder says what each cell measures
and what a wrong-looking answer looks like, so a cell that fails is still informative.

AMD report that on a Strix Halo box set to 96 GiB of graphics memory,
llama.cpp reads the carve-out as shared iGPU memory and large models
will not load, and that ROCm refuses allocations above 64 GB.

Our CI runner is a 128 GB Strix Halo with a 64 GiB carve-out, and at
that size neither claim reproduces: ggml sums the Vulkan heaps to
111.65 GiB, which stays under the 127.65 GiB the machine holds, so
nothing impossible is claimed and a 77 GiB model loads; and a single
HIP allocation reaches 110.2 GiB rather than stopping at 64. The
over-report is a function of the carve-out size, so it needs a box
configured the way AMD configured theirs.

This kit is what to run on such a box. run.ps1 walks six cells: the
host facts, two stock llama.cpp release builds, the memory report,
a bounded largest-allocation search, Studio's own Vulkan probe at a
pinned commit, and llama-fit-params on both backends. Every cell
writes JSON and report.py turns the whole directory into one table
with the three statements stated as yes or no against the machine's
real memory.

The allocation search stops at 92 percent of physical RAM on purpose.
AMD's own note warns that an allocation near 100 percent of usable RAM
can hang the host or bugcheck it, and the question here is whether an
allocation the stock runtime refused now succeeds, not what the new
ceiling is.

Throwaway: this is a diagnostic to run once, not something to keep.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-07T05:17:25.193635Z f93deb9 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f93deb9f7a

ℹ️ 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".

except Exception as e: # noqa: BLE001
info["error"] = f"{type(e).__name__}: {e}"

bin_dir = find_bin_dir(args.dest) if args.dest.is_dir() else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Abort instead of accepting binaries left by an earlier run

When a download or extraction fails, this still searches the persistent destination and returns success if an earlier run left a llama-server there. Because run.ps1 reuses $Work\lcpp\rocm and $Work\lcpp\vulkan even when -Tag changes, a transient failure can silently run an old build while the JSON records the newly requested URL, invalidating the experiment; skip this lookup after an exception or clean/use a tag-specific destination.

Useful? React with 👍 / 👎.

Comment on lines +105 to +106
if h["ram_gib"] is not None:
physical = round(h["ram_gib"] + (h["registry_vram_gib"] or 0), 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep over-report undecided without the carve-out

When the registry probe cannot read qwMemorySize, treating the missing carve-out as zero compares ggml's total against only Windows-visible RAM and can report over_report: yes even for an honest reading. This directly contradicts run.ps1's warning that the statement requires the carve-out; require both ram_gib and registry_vram_gib before constructing physical.

Useful? React with 👍 / 👎.

Comment on lines +84 to +86
info["archive_bytes"] = archive.stat().st_size
extract(archive, args.dest)
archive.unlink(missing_ok = True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash release archives before deleting them

The release fetch records only the archive size and then deletes it, so the returned directory cannot verify which ROCm or Vulkan release asset was tested if an asset at the pinned tag is replaced. This undermines the README's explicit promise that both builds are hashed and that the exact binaries can be re-checked; record an archive digest, or hashes of the relevant extracted binaries, before unlinking the archive.

Useful? React with 👍 / 👎.

Comment on lines +94 to +96
vk_raw = (sec(doc, "vulkan_raw").get("devices") or [None])[0]
ggml = (sec(doc, "ggml_vulkan").get("devices") or [None])[0]
hip = (sec(doc, "hip").get("devices") or [None])[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Select the Strix iGPU instead of assuming device zero

On a mixed APU+dGPU host, these first-element selections can evaluate all three statements against the discrete GPU rather than the Strix Halo device, producing an irrelevant negative result even though the raw inventories contain the target. This is a known supported topology: studio/install_llama_prebuilt.py:2438-2440 explicitly documents Strix Halo plus RX 7900 hosts and warns that a bare first match chooses the wrong device; match the collected device name/type/is_igpu fields instead of assuming ordinal zero.

Useful? React with 👍 / 👎.

Comment on lines +175 to +176
if doc.get("_error"):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail required checks when an input report is unreadable

When a JSON path is missing or malformed, this branch skips the document without adding any labels to undecided, so even report.py missing.json --require over_report exits successfully with an empty statements table. That defeats the purpose of --require and the module's promise that missing required readings return nonzero; mark each requested statement undecidable for this document or return failure immediately.

Useful? React with 👍 / 👎.

@danielhanchen

Copy link
Copy Markdown
Member Author

Ran the kit on a Strix Halo box at three carve-outs, including 96 GiB

Hardware: Ryzen AI Max+ PRO 395 / Radeon 8060S (gfx1151), 128 GB, Windows 11, driver 32.0.31041.1004, llama.cpp b10798-mix-659e406 stock ROCm + Vulkan builds. Carve-out changed in firmware (Advanced -> Built-In Device Options -> Dedicated Graphics Memory) and rebooted between arms. All throughput on AC power.

Headline: over_report is NO at 96 GiB. The claim does not reproduce at AMD's own configuration.

The three statements, across three carve-outs

carve-out visible RAM Vulkan heaps ggml total machine over_report
32 GiB 95.78 36.21 + 72.41 108.62 127.78 NO
64 GiB (CI runner) 63.65 37.22 + 74.43 111.65 127.65 NO
96 GiB 31.78 15.89 + 96.00 111.89 127.78 NO

sums_heaps yes at all three (confirmed to the byte). hip_is_vgm_only NO at all three.

ggml's number never exceeds the machine. This PR's body predicts that at 96 GiB "the same call is expected to report more memory than the machine physically has, which is what AMD saw". Measured, it reports 111.89 GiB against 127.78 GiB. That prediction should be rewritten now that the configuration has an answer.

The heap structure does change: at 96 GiB the device-local heap is exactly the carve-out (96.00) and the other is exactly half of visible RAM (31.78 / 2 = 15.89). Cleaner than at the smaller carve-outs, but still summing to less than the machine.

Two independent probe runs at 96 GiB produced byte-identical numbers.

Allocation cap: no 64 GB clamp

At the 32 GiB carve-out, largest single hipMalloc that memset and read back at three offsets: 107.58 GiB (smallest failure 108.04, boundary stable across two repetitions each side, all failures a clean rc=2 out of memory). At 96 GiB, ROCm advertises 107.92 GiB. There is no 64 GB constant anywhere.

The vendor rule quoted in the body, max(dedicated_VRAM_heap, 0.75 * shared_GART_heap), does not fit this box either: it predicts 72.41 GiB (dedicated 72.41; 0.75 x 36.21 = 27.16) and 107.58 GiB was measured. Also worth noting HIP handed out 107.58 GiB while hipMemGetInfo reported a 89.47 GiB total, so its reported total is not a bound on what it will allocate.

What actually costs the user: the carve-out, by 3-4x

llama-bench, Qwen3-Next-80B-A3B-Instruct UD-Q4_K_XL (42.90 GiB), -ngl 999, 5 reps, AC:

32 GiB carve-out 96 GiB carve-out speedup
ROCm pp512 150.88 +/- 2.44 579.68 +/- 8.70 3.84x
ROCm tg128 11.58 +/- 0.75 46.70 +/- 4.75 4.03x
Vulkan pp512 169.47 +/- 6.24 588.33 +/- 34.24 3.47x
Vulkan tg128 18.17 +/- 0.62 54.57 +/- 0.40 3.00x

At 32 GiB the model does not fit the carve-out and runs from shared system memory; at 96 GiB it is entirely carve-out resident. Whether the model fits the carve-out is worth 3-4x — far more than the backend choice (10-17%) and infinitely more than the memory reporting (nothing).

Backend: the advantage is real but small, and reverses on memory

llama-server --list-devices, and llama-fit-params on GLM-5.3-Flash UD-IQ1_S (86.69 GiB):

32 GiB carve-out 96 GiB carve-out
ROCm advertised / free 89.47 / 89.31 GiB 107.92 / 107.76 GiB
Vulkan advertised / free 108.62 / 103.19 GiB 111.89 / 106.29 GiB
GLM context, ROCm -c 160256 -c 930816
GLM context, Vulkan -c 735232 -c 862976
winner Vulkan, 4.59x ROCm, +7.9%

At a small carve-out ROCm starves large models (19.16 GiB less, 4.59x less context). At 96 GiB that inverts: Vulkan advertises more total but reserves more, so ROCm has the larger free pool and fits more context. The ROCm memory disadvantage is a small-carve-out phenomenon.

Throughput at 96 GiB on AC, two models, -ngl 999:

model metric ROCm Vulkan
Qwen3-Next-80B UD-Q4_K_XL (42.90 GiB) pp512 579.68 +/- 8.70 588.33 +/- 34.24 (+1.5%, inside error)
tg128 46.70 +/- 4.75 54.57 +/- 0.40 (+16.9%)
Qwen3.8-Flash-Next UD-IQ1_S (67.56 GiB) pp512 326.94 +/- 11.10 324.71 +/- 4.27 (-0.7%, tied)
tg128 25.21 +/- 0.71 29.17 +/- 0.44 (+15.7%)

Prefill is a tie at 96 GiB; Vulkan leads decode by ~16% on both models. Note this is not the +22.9% prefill / +8.3% decode recorded on the CI runner, and at the 32 GiB carve-out on a 313B model ROCm actually decoded 26% faster than Vulkan (3.50 +/- 0.04 vs 2.77 +/- 0.12). The speed advantage is model- and configuration-dependent and should not be quoted as a single number.

Lemonade comparison

AMD reported Lemonade working well on this hardware. Lemonade 11.9.0's server on this machine resolves llamacpp.backend = auto to vulkan (/api/v1/system-info reports llamacpp default_backend: vulkan). Mainline Studio before #10381 routed gfx1151 to ROCm. So that comparison was Vulkan-Lemonade against ROCm-Studio.

Running the identical benchmark through Lemonade's own bundled build, same model, same flags, same machine:

Qwen3.8-Flash-Next UD-IQ1_S (67.56 GiB) pp512 tg128
Lemonade Vulkan (b10723) 209.60 +/- 0.59 25.59 +/- 2.09
Unsloth Vulkan (b10798) 324.71 +/- 4.27 29.17 +/- 0.44
delta +54.9% +14.0%

The Unsloth prebuilt is substantially faster than Lemonade's on the same backend. Lemonade ships b10723 against our b10798, so at least part of this is build vintage rather than anything either project configures.

Issues found

  1. This PR's central prediction is falsified at 96 GiB and needs rewriting.
  2. Cell 4's ceiling is unsafe on a small carve-out. It bounds at 92% of visible + carve-out, which was 117.6 GiB against 95.78 GiB of visible RAM. The successful 107.58 GiB attempt took 308 seconds for hipMalloc alone (versus ~10 s for the same size on retry) because the host was paging, and it froze the desktop for minutes — four service crashes in the event log during the window. The README justifies this bound as avoiding a host hang; on a small carve-out it guarantees heavy paging instead. Suggest bounding against visible RAM, or aborting an attempt whose malloc_seconds shows it has gone to disk.
  3. Studio: take GGML_CUDA_ENABLE_UNIFIED_MEMORY only when the weights outgrow the APU carve-out #10351 may not cover the case its body cites. It says the fix applies to "every published Qwen3.8-Flash-Next quant on a 128 GB Strix Halo with the usual 64 GB carve-out", but the smallest published quant, UD-IQ1_S, is 67.56 GiB and every other one is larger. Since _unified_memory_would_help takes the variable when weights exceed the carve-out, those launches still get GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 on a 64 GiB carve-out. Worth re-checking against whatever quant the reporters actually ran.
  4. The backend migration is opt-in. An existing ROCm install does not move to Vulkan on update; the flavour is pinned in UNSLOTH_PREBUILT_INFO.json and only changes when the user takes the banner. If they ever chose ROCm by hand, backend_request != "auto" and the migration is never offered. Anyone retesting on "latest" may still be on ROCm.

Forcing unified memory at a large carve-out is catastrophic

Not something Studio does, but worth recording. ROCm at the 96 GiB carve-out with GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 on the 42.90 GiB model drove free RAM to 0.01 GiB and had to be killed. Managed allocation pulls the weights into the 31.78 GiB of visible host RAM rather than the 96 GiB carve-out.

Studio correctly avoids this: _unified_memory_would_help requires host_mib > pool_mib, and at a 96 GiB carve-out host RAM is far smaller than the pool. On Windows it cannot fire at all, since _rocm_selected_pool_mib needs torch-ROCm.

This does make unslothai/llama.cpp#149 look worth landing — GGML_CUDA_ENABLE_UNIFIED_MEMORY=0 currently enables the variable because both read sites presence-test it, so anyone who tried to turn it off got the opposite. #157 (cudaMemcpyDefault in the 2D copy fast path) and #158 (prop.integrated races on HIP integrated GPUs, reported on gfx1151 as corrupted output and a perplexity jump from 7.72 to 3024) both look relevant to this hardware too.

Not established

  • Any Studio-specific failure at 96 GiB. Neither AMD's proposed mechanism nor the backend-routing one survives there.
  • Whether AMD's report was Windows or Linux, and at what carve-out. Every effect found here lives at small carve-outs, where three separate penalties stack (3-4x for not fitting the carve-out, 19 GiB from ROCm routing, and pre-Studio: take GGML_CUDA_ENABLE_UNIFIED_MEMORY only when the weights outgrow the APU carve-out #10351 unified memory) and all three vanish at 96 GiB. Worth asking them for the carve-out, the OS, and what "not optimal" meant numerically — a load failure, a context limit, or tokens/sec. That single answer discriminates every remaining hypothesis.

@danielhanchen

Copy link
Copy Markdown
Member Author

Follow-up: a correctness result that inverts the Lemonade comparison

Same machine as the previous comment (gfx1151, 128 GB, Windows), now with the carve-out at 96 GiB. This is about output correctness rather than memory, and it changes what "Lemonade works well, Studio does not" can mean on this hardware.

There is a known corruption on HIP integrated GPUs from c7d87229 (ggml-org#24233, merged 2026-07-16), affecting b10040 onward. The fix, ggml-org#25863, is still open upstream. It triggers whenever prompt_tokens > n_ubatch, and the stock flags are -b 2048 -ub 512, so ordinary use is already in the corrupting regime.

The A/B is one flag on one binary — -c 2048 -b 2048 with -ub 512 against -ub 2048 — on Llama-3.2-1B, wikitext-2, 8 chunks:

build -ub 512 -ub 2048 (control) verdict
ggml-org b10766 win-rocm-7.14 17675.29 11.7167 BROKEN
Lemonade 11.9.0 llamacpp:rocm b10707 17813.03 11.7167 BROKEN
unsloth b10333-mix / b10472-mix 17795.02 11.7140 BROKEN
unsloth b10798-mix-659e406 (current) 11.7071 11.7140 CLEAN

~1500x, gone by changing -ub alone. Full detail and caveats are on unslothai/llama.cpp#158.

On this hardware, Lemonade's ROCm path produces numerically corrupt output at default settings and the current Unsloth prebuilt does not. The fair qualifier: Lemonade defaults to llamacpp:vulkan on gfx1151 (confirmed earlier from its own /api/v1/system-info), and Vulkan is unaffected — so a default Lemonade user is fine. But anyone who selects its ROCm backend is not.

Combined with the earlier throughput result (Unsloth's Vulkan build +54.9% prefill / +14.0% decode over Lemonade's on the same model), the "Lemonade works, Unsloth does not" framing does not survive measurement on this machine in either dimension.

Also confirmed here, relevant to unslothai/llama.cpp#149: GGML_CUDA_ENABLE_UNIFIED_MEMORY=0 enables managed memory on mainline b10766 — it starved the host to 5.21 GiB free and had to be killed — while the same flag on the Unsloth prebuilt behaves as unset. At a 96 GiB carve-out this is a host denial of service rather than a slowdown, because managed pages come from the 31.78 GiB of visible RAM instead of the carve-out.

Correction to my previous comment's framing

My first perplexity attempt used 512-token chunks against -ub 512, so every chunk fit a single ubatch — the configuration that is always clean, on broken builds too. It could not have detected the defect, and I initially read it as "clean". Same mistake in the first cross-slot test, which used 15-token prompts. Only the reruns with prompt_tokens > n_ubatch count, and the harness is now validated by a known-broken positive control.

One more trap worth flagging for anyone reproducing this: the ggml-org rocm-* packages ship ggml-hip.dll but no ROCm math libraries. Without a matching runtime staged beside them, --list-devices prints (none) and they fall back to CPU silently, returning a perfectly clean perplexity that means nothing. Check for ROCm : NO_VMM = 1 in system_info before trusting any number from those builds.

What is still open on the AMD question

Nothing here reproduces a Studio-specific failure at a 96 GiB carve-out. The memory statements are NO at 32, 64 and 96 GiB; the ROCm memory shortfall inverts at 96 GiB; and the corruption above is not Studio-specific, it is upstream and affects Lemonade too. The single most useful thing would still be AMD's carve-out, OS, and what "not optimal" meant numerically.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant