You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Eval bug: Decreased perfomance because of input layers on CPU for AMD Strix Halo #25700
version 1 (1705d43)
build with Clang 23.0.0 for Windows
Operating systems
Windows
GGML backends
HIP
Hardware
AMD Strix Halo 128GB
Models
Qwen 3.6 35B MoE Q8
Problem description & steps to reproduce
I was fighting with 30% cpu usage and decreased GPU utilization on AMD Strix halo machine when running Qwen 3.6 35B MoE Q8 generation on ROCm. Vulkan does not have that problem and needs only ~5% cpu to run tg.
GLM found this issue in src/llama-model.cpp around line 1300:
// there is very little benefit to offloading the input layer, so always keep it on the CPU
pimpl->dev_input = { cpu_dev, &pimpl->cpu_buft_list };
This keeps tok_embd on CPU even with -ngl 99, which forces GET_ROWS(tok_embd, inp_tokens) onto the CPU backend at the start of every forward pass. On Stix Halo where CPU and GPU share same DDR5 it hurts a lot.
Possible fix is to mirror the output-layer policy for the input layer when full offload is requested.
Before fix:
41.33 t/s tg (30% CPU usage)
After full GPU offload:
47.24 t/s tg (2% CPU usage)
Sorry im not that yet deep into llm engine programming, but this is what my llm says:
Vulkan does not suffer because its async H2D copy actually works (copy_from_host=true), so 2nd split only adds minimal overhead.
Rocm HIP backend hardcodes copy_from_host=false (with comment 'this change is not beneficial for hip') forcing a fallback path that does sync(source) + sync(dst) + copy per input tensor - 10 inputs * 2 syncs = 20 syncs per token, what kills TG perfomance.
Dunno, different buffer management? Might be a similar issue as to how --mmap is unusable on ROCm right now while it works fine on Vulkan.
there are few issues there too, from my attempts to hack around windows memory limits i found that mapping actually does not map but copies memory to gpu area, doubling memory usage basically
Why would you not want to offload input tensors to the device in case of iGPU? The only rationale for not doing that is to save memory, that doesn't apply in case of iGPU systems.
Why would you not want to fix async behaviour of the backend? I'm not saying this should not be looked at, just that first priority for fixes is always within the backend.
I prepared agent workflow for a friend, but it needs sanity check since made by ai: https://pastebin.com/dBDRuJku
I will apply these patches step by step against main branch later, since i used strix halo fork for main work.
im still investigating this issue from different sides.
apparently on windows adding --poll 0 for llama-bench alsmost fixes this problem and increases tg1024 40.8->47
applied fixes still contribute to minor boost for Q8 MoE model adding +60pp and +0.9tg
llama-server does not have increased CPU usage because it does additional work and have no cpu load show up, unlike llama-bench
Added all current patches to https://github.com/Vasili-Sk/llama.cpp/tree/win-tg-perfomance against mainstream that verified any benefit
Main issue was --poll 50 in windows for llama-bench caused 30% CPU load that degraded performance down to 41 tg, not visible when running llama-server. Currently it is still behind on pp from strix halo fork, it is under investigation
@Vasili-Sk — excellent benchmarking work here, exactly the kind of data needed to move this forward.
We're running the same hardware (AMD Strix Halo gfx1151, Windows 11, 32 GB unified RAM) and hit the same CPU-offload performance wall. We've confirmed the baseline upstream behavior on LM Studio's bundled llama-server (v2.25.2 runtime) and are eager to test your win-tg-perfomance branch patches.
Would you be willing to share a compiled Windows binary (llama-server.exe + any required HIP/ROCm DLLs) built from your win-tg-perfomance branch? We'd like to drop it into LM Studio's engine folder and validate the gains on our rig while we wait for these fixes to land upstream and get bundled into a release.
If you'd prefer not to distribute a full build, even a pointer to your build flags/toolchain would help us compile it ourselves. We saw you shared an agent workflow on pastebin — happy to follow a similar guide if one exists for the Windows ROCm build.
Also flagging a related issue we filed today that may be of interest: #26089 (VRAM enumeration returning 0 on Windows Strix Halo when BIOS GPU allocation is set to Auto — affects both ROCm and Vulkan).
Backend tested: Lemonade b10236, TheRock ROCm 7.13.0 for gfx1151. Lemonade automatically sets OCL_SET_SVM_SIZE=262144 for this arch.
Observed:
GPU VRAM showed ~26 GB used (Lemonade UI) despite 96 GB available and a 54.4 GB model
Remaining ~28 GB of model loaded into the 32 GB system RAM pool — visible system stutter as a result
TTFT for 22K token prefill: ~5 minutes before Hermes client timed out
Estimated prefill throughput: ~82 t/s
Contrast — Vulkan b10241 on the same hardware/model:
Full 54.4 GB loaded into GPU memory, no system RAM pressure, no stutter
TTFT for same 22K token prefill: 109 seconds
Prefill throughput: 202 t/s average (364 t/s peak at 2K tokens)
Generation: 33.25 t/s
The partial offload to system RAM on the ROCm path appears to be distinct from (or in addition to) the input-layer CPU sync overhead described in the original report. Even with OCL_SET_SVM_SIZE=262144 and TheRock's gfx1151-specific stack, the ROCm allocator appears to be capping GPU allocation well below the available 96 GB for this model size.
@Vasili-Sk@pwilkin I have confirmed the root cause. the line 1287 in src/llama-model.cpp hardcodes dev_input to CPU , on UMA architectures (Strix Halo, other iGPUs, Radeon 890M) this causes cross-backend synchronization overhead on every forward pass even though CPU and GPU share the same physical memory.
the output layer (dev_output) already uses get_layer_buft_list() which follows -ngl.
what i think a fix for dev_input would be:
when i_gpu_start == 0 (full offload requested), assign dev_input via get_layer_buft_list(0) instead of hardcoding CPU. I think this will handle the offloads on discrete GPUs.
@sanjayahari — this matches exactly what we're seeing on AMD Ryzen AI MAX+ 395 (Strix Halo gfx1151), Windows 11, 32 GB unified RAM, using LM Studio's bundled llama-server (v2.27.1 runtime).
Baseline TG on our hardware via the OpenAI-compatible API (Qwen3.5-9B Q6_K, 400 tokens):
ROCm 2.27.1: ~27 tok/s
Vulkan 2.27.1: ~27 tok/s (parity after the prop.integrated fix)
The \dev_input\ hardcode to CPU as the root cause is consistent with what we observed — the gap between ROCm and Vulkan closed in 2.27.x, but both backends are likely still paying the cross-backend sync cost you describe on every forward pass.
We can reproduce on Windows with this exact hardware class and are happy to test a compiled build or a patch against 2.27.1 if you or @Vasili-Sk can share one. We have a working build environment and can run llama-bench against the fix to provide before/after numbers.
(Related: we also filed lmstudio-ai/lms#589 tracking the VRAM enumeration failure on this hardware — same root platform, different symptom.)
Adding a data point with a Vulkan control arm on the same machine, which may help separate the input-layer policy from HIP-specific overhead.
Setup: Strix Halo (gfx1151), Linux, Docker. Same model on both arms (bartowski/Ornith-1.5-35B-A3B Q4_K_M, a 35B-A3B MoE), same build b10666-4e97ac86e, same per-slot context (131072), -ngl 999 on both. Official images server-rocm-b10666 and server-vulkan-b10666. Measured via /completions, cache_prompt=false, temp=0, one arm at a time.
CPU time per generated token, as a cgroup usage_usec delta across a 300-token generation:
backend
tg
CPU ms/token
busy cores
Vulkan
47.9 t/s
8.25
0.39
ROCm
41.3 t/s
45.49
1.86
5.5x more CPU time per token on ROCm. Since the llama-model.cpp policy this issue describes is backend-agnostic, both arms should pay the input-layer GET_ROWS cost equally — so a difference this large suggests most of the ROCm CPU load is HIP runtime synchronisation (the sustained ~1.9 busy cores while the GPU is the bottleneck look like spin-waiting), with the input layer being a smaller, shared component on top.
That doesn't argue against the fix proposed here — it would help both backends — but it may mean the CPU usage reported in the OP has two independent contributors.
Throughput and power during generation (400 tokens, GPU clock/power sampled at 0.2 s):
backend
tg
sclk mean
busy%
package power
Vulkan
47.8 t/s
2624 MHz
83%
101.1 W
ROCm
40.3 t/s
2663 MHz
85%
115.9 W
Worth noting the iGPU clocks slightly higher under ROCm at comparable occupancy, so on this shared-TDP part the CPU spin is not visibly starving the GPU of clock headroom — at equal clock and equal busy% ROCm just yields fewer tokens. The ~15 W package delta lines up with the ~1.5 extra busy cores. Net efficiency is 0.47 vs 0.35 tokens/joule.
One coincidence that may or may not mean anything: the 41.33 → 47.24 t/s reported in the OP brackets our two arms almost exactly (ROCm 41.3, Vulkan 47.8). If that holds up, the fix here would put ROCm generation roughly where Vulkan already is on this hardware.
Caveats: both containers were resident simultaneously (~20 GB mlock'd each), so absolute numbers are depressed by memory pressure; the relative comparison should hold since the idle arm was not computing. Single runs, not averaged over repeats. Happy to re-run anything specific, or with only one container loaded, if that would be useful.
Linux data point with the fix gated to integrated GPUs only (ggml_backend_dev_type == GGML_BACKEND_DEVICE_TYPE_IGPU and full offload), which addresses the "detrimental to non-UMA" concern in #27426 by construction:
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
index e195f50d0..661c3da0f 100644
--- a/src/llama-model.cpp+++ b/src/llama-model.cpp@@ -1533,8 +1533,17 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
};
// assign the input layer
- // there is very little benefit to offloading the input layer, so always keep it on the CPU+ // there is very little benefit to offloading the input layer, so keep it on the CPU --+ // except on integrated GPUs with full offload: CPU and GPU share one memory, so the+ // CPU copy saves nothing, while the CPU GET_ROWS costs a cross-backend sync every+ // forward pass (#25700). Discrete GPUs are unaffected by construction.
pimpl->dev_input = { cpu_dev, &pimpl->cpu_buft_list };
+ if (act_gpu_layers > n_layer_all) {+ const auto first = get_layer_buft_list(0);+ if (first.dev != cpu_dev && ggml_backend_dev_type(first.dev) == GGML_BACKEND_DEVICE_TYPE_IGPU) {+ pimpl->dev_input = first;+ }+ }
// assign the repeating layers to the devices according to the splits
pimpl->dev_layer.resize(n_layer_all);
The fix is effective (the ROCm0 buffer grows by 515 MiB, token_embd now on the GPU) and PPL is unchanged, but on current master there is no measurable gain. Possibly #28604 (host buffers disabled on iGPUs) removed the sync cost the original reports saw. The high CPU usage on HIP remains either way (~1 core at 49 t/s vs 6.6 ms/token = 0.4 core on Vulkan, same model), so that part isn't the input layer. Can someone who saw the 41→47 t/s gain re-check on a build after #28604?
Activity
Mushoz commentedon Jul 15, 2026
Oh this would explain why ROCm is behind compared to Vulkan on token generation speeds. Good find!
0cc4m commentedon Jul 15, 2026
Why would this affect ROCm but not Vulkan? It also runs on CPU for Vulkan then.
pwilkin commentedon Jul 15, 2026
Dunno, different buffer management? Might be a similar issue as to how
--mmapis unusable on ROCm right now while it works fine on Vulkan.Vasili-Sk commentedon Jul 15, 2026
Sorry im not that yet deep into llm engine programming, but this is what my llm says:
Vulkan does not suffer because its async H2D copy actually works (copy_from_host=true), so 2nd split only adds minimal overhead.
Rocm HIP backend hardcodes copy_from_host=false (with comment 'this change is not beneficial for hip') forcing a fallback path that does sync(source) + sync(dst) + copy per input tensor - 10 inputs * 2 syncs = 20 syncs per token, what kills TG perfomance.
pwilkin commentedon Jul 15, 2026
Yeah, makes sense. Basically, it makes sense to only offload input tensors to CPU for non-integrated devices.
Vasili-Sk commentedon Jul 15, 2026
there are few issues there too, from my attempts to hack around windows memory limits i found that mapping actually does not map but copies memory to gpu area, doubling memory usage basically
0cc4m commentedon Jul 15, 2026
First step should be to check the ROCm backend's assumption about async copies, not conditional llama.cpp core changes.
pwilkin commentedon Jul 15, 2026
Why would you not want to offload input tensors to the device in case of iGPU? The only rationale for not doing that is to save memory, that doesn't apply in case of iGPU systems.
0cc4m commentedon Jul 15, 2026
Why would you not want to fix async behaviour of the backend? I'm not saying this should not be looked at, just that first priority for fixes is always within the backend.
11 remaining items
0cc4m commentedon Jul 15, 2026
I can't reproduce any performance increase from this on Linux with Strix Halo.
pwilkin commentedon Jul 15, 2026
@Vasili-Sk can you post the patches that give you the improved performance measures?
Vasili-Sk commentedon Jul 15, 2026
I prepared agent workflow for a friend, but it needs sanity check since made by ai:
https://pastebin.com/dBDRuJku
I will apply these patches step by step against main branch later, since i used strix halo fork for main work.
Vasili-Sk commentedon Jul 16, 2026
im still investigating this issue from different sides.
apparently on windows adding --poll 0 for llama-bench alsmost fixes this problem and increases tg1024 40.8->47
applied fixes still contribute to minor boost for Q8 MoE model adding +60pp and +0.9tg
llama-server does not have increased CPU usage because it does additional work and have no cpu load show up, unlike llama-bench
pwilkin commentedon Jul 16, 2026
So here's the issue - I've tried adding all the fixes here: https://github.com/pwilkin/llama.cpp/commits/fix-igpu-input-layer/
I even added the missing CUDA kernels so that the entire graph now lives on ROCm - including
GET_ROWS.However, I haven't been able to reproduce any of the performance gains - in fact, the performance has regressed a bit compared to mainline.
Vasili-Sk commentedon Jul 16, 2026
which OS?
pwilkin commentedon Jul 16, 2026
Linux.
Vasili-Sk commentedon Jul 17, 2026
Added all current patches to https://github.com/Vasili-Sk/llama.cpp/tree/win-tg-perfomance against mainstream that verified any benefit
Main issue was --poll 50 in windows for llama-bench caused 30% CPU load that degraded performance down to 41 tg, not visible when running llama-server. Currently it is still behind on pp from strix halo fork, it is under investigation
Patch Decomposition Report — Strix Halo (gfx1151) ROCm on Upstream Master
Test rig
llama.cppupstream master @e8f19cc0a(b9996-era), branchwin-tg-perfomance-p 16384 -n 1024 -ngl 99 -ctk q8_0 -ctv q8_0 -fa 1 -b 4096 -ub 2048 -r 3 --mmap 0 --no-host 1 --poll 0llama-bench, results saved with git SHA baked into filename.Patches deemed already-shipped upstream (SKIPPED — no source change)
integrated = prop.integratedfor HIPc7d872292"ggml-cuda : restore prop.integrated on HIP builds (#24233)" (liminfei-amd, 2026-07-16)copy_from_hostfor HIPcpy_tensor_asyncis now CUDA-to-CUDA onlyOutcome per state (live)
e8f19cc0a9ac96fe629c4afcf6bCumulative scoreboard (vs baseline upstream, --poll 0)
cflury4784 commentedon Jul 24, 2026
@Vasili-Sk — excellent benchmarking work here, exactly the kind of data needed to move this forward.
We're running the same hardware (AMD Strix Halo gfx1151, Windows 11, 32 GB unified RAM) and hit the same CPU-offload performance wall. We've confirmed the baseline upstream behavior on LM Studio's bundled llama-server (v2.25.2 runtime) and are eager to test your
win-tg-perfomancebranch patches.Would you be willing to share a compiled Windows binary (
llama-server.exe+ any required HIP/ROCm DLLs) built from yourwin-tg-perfomancebranch? We'd like to drop it into LM Studio's engine folder and validate the gains on our rig while we wait for these fixes to land upstream and get bundled into a release.If you'd prefer not to distribute a full build, even a pointer to your build flags/toolchain would help us compile it ourselves. We saw you shared an agent workflow on pastebin — happy to follow a similar guide if one exists for the Windows ROCm build.
Also flagging a related issue we filed today that may be of interest: #26089 (VRAM enumeration returning 0 on Windows Strix Halo when BIOS GPU allocation is set to Auto — affects both ROCm and Vulkan).
WareWolf-MoonWall commentedon Aug 7, 2026
Hardware: AMD Ryzen AI MAX+ 395 (gfx1151 / Strix Halo), 128 GB unified LPDDR5X — ROCm reports 96 GB as GPU VRAM, 32 GB reserved for system.
Model: Laguna APEX compact — 54.4 GB, 3.70 BPW, 118B total / ~8B active MoE.
Backend tested: Lemonade b10236, TheRock ROCm 7.13.0 for gfx1151. Lemonade automatically sets
OCL_SET_SVM_SIZE=262144for this arch.Observed:
Contrast — Vulkan b10241 on the same hardware/model:
The partial offload to system RAM on the ROCm path appears to be distinct from (or in addition to) the input-layer CPU sync overhead described in the original report. Even with
OCL_SET_SVM_SIZE=262144and TheRock's gfx1151-specific stack, the ROCm allocator appears to be capping GPU allocation well below the available 96 GB for this model size.sanjayahari commentedon Aug 9, 2026
@Vasili-Sk @pwilkin I have confirmed the root cause. the line 1287 in src/llama-model.cpp hardcodes dev_input to CPU , on UMA architectures (Strix Halo, other iGPUs, Radeon 890M) this causes cross-backend synchronization overhead on every forward pass even though CPU and GPU share the same physical memory.
the output layer (dev_output) already uses get_layer_buft_list() which follows -ngl.
what i think a fix for dev_input would be:
when i_gpu_start == 0 (full offload requested), assign dev_input via get_layer_buft_list(0) instead of hardcoding CPU. I think this will handle the offloads on discrete GPUs.
cflury4784 commentedon Aug 10, 2026
@sanjayahari — this matches exactly what we're seeing on AMD Ryzen AI MAX+ 395 (Strix Halo gfx1151), Windows 11, 32 GB unified RAM, using LM Studio's bundled llama-server (v2.27.1 runtime).
Baseline TG on our hardware via the OpenAI-compatible API (Qwen3.5-9B Q6_K, 400 tokens):
The \dev_input\ hardcode to CPU as the root cause is consistent with what we observed — the gap between ROCm and Vulkan closed in 2.27.x, but both backends are likely still paying the cross-backend sync cost you describe on every forward pass.
We can reproduce on Windows with this exact hardware class and are happy to test a compiled build or a patch against 2.27.1 if you or @Vasili-Sk can share one. We have a working build environment and can run llama-bench against the fix to provide before/after numbers.
(Related: we also filed lmstudio-ai/lms#589 tracking the VRAM enumeration failure on this hardware — same root platform, different symptom.)
hgnnmnn commentedon Aug 28, 2026
Adding a data point with a Vulkan control arm on the same machine, which may help separate the input-layer policy from HIP-specific overhead.
Setup: Strix Halo (gfx1151), Linux, Docker. Same model on both arms (
bartowski/Ornith-1.5-35B-A3BQ4_K_M, a 35B-A3B MoE), same buildb10666-4e97ac86e, same per-slot context (131072),-ngl 999on both. Official imagesserver-rocm-b10666andserver-vulkan-b10666. Measured via/completions,cache_prompt=false,temp=0, one arm at a time.CPU time per generated token, as a cgroup
usage_usecdelta across a 300-token generation:5.5x more CPU time per token on ROCm. Since the
llama-model.cpppolicy this issue describes is backend-agnostic, both arms should pay the input-layerGET_ROWScost equally — so a difference this large suggests most of the ROCm CPU load is HIP runtime synchronisation (the sustained ~1.9 busy cores while the GPU is the bottleneck look like spin-waiting), with the input layer being a smaller, shared component on top.That doesn't argue against the fix proposed here — it would help both backends — but it may mean the CPU usage reported in the OP has two independent contributors.
Throughput and power during generation (400 tokens, GPU clock/power sampled at 0.2 s):
Worth noting the iGPU clocks slightly higher under ROCm at comparable occupancy, so on this shared-TDP part the CPU spin is not visibly starving the GPU of clock headroom — at equal clock and equal busy% ROCm just yields fewer tokens. The ~15 W package delta lines up with the ~1.5 extra busy cores. Net efficiency is 0.47 vs 0.35 tokens/joule.
One coincidence that may or may not mean anything: the 41.33 → 47.24 t/s reported in the OP brackets our two arms almost exactly (ROCm 41.3, Vulkan 47.8). If that holds up, the fix here would put ROCm generation roughly where Vulkan already is on this hardware.
Caveats: both containers were resident simultaneously (~20 GB mlock'd each), so absolute numbers are depressed by memory pressure; the relative comparison should hold since the idle arm was not computing. Single runs, not averaged over repeats. Happy to re-run anything specific, or with only one container loaded, if that would be useful.
docs(adr): record ROCm A/B measurements in ADR 0003
huppiflupp commentedon Sep 19, 2026
Linux data point with the fix gated to integrated GPUs only (
ggml_backend_dev_type == GGML_BACKEND_DEVICE_TYPE_IGPUand full offload), which addresses the "detrimental to non-UMA" concern in #27426 by construction:Strix Halo (395, gfx1151), Linux 7.2, master
ec92815, llama-server/completion, 3×300 tokens:The fix is effective (the ROCm0 buffer grows by 515 MiB,
token_embdnow on the GPU) and PPL is unchanged, but on current master there is no measurable gain. Possibly #28604 (host buffers disabled on iGPUs) removed the sync cost the original reports saw. The high CPU usage on HIP remains either way (~1 core at 49 t/s vs 6.6 ms/token = 0.4 core on Vulkan, same model), so that part isn't the input layer. Can someone who saw the 41→47 t/s gain re-check on a build after #28604?