ggml : fix view init skipped after buft max_size split - #25584
Conversation
Tested this on the hardware and model that #21576 was opened for. TL;DR: the assert is gone and the saved state is complete again Setup
37-token prompt into slot 0, then Results
Both aborts are * that row is from my earlier run reported in #21576, not rebuilt today; every other row comes from this one checkout. The byte count: 8,337,184 is what the non-splitting path writes. This PR reproduces that number exactly in both splitting configurations, so whether the allocation splits no longer changes what ends up in the state. That is the invariant that was broken. The guards in #21576 write 151,576 bytes less. That is exactly one layer's K plus V for 37 cells (2 × 37 × 1024 × 2 B, plus 24 B of header): the last layer is silently dropped, which is what @liminfei-amd predicted from fault injection. So this PR does not merely remove the assert, it restores the data the guards were throwing away. Restore round-trips in every passing case:
On the
|
|
Follow-up on local A/B testing after an AMD driver update cleared the earlier Same command, 10 runs each on Vulkan:
I also updated the PR Test plan accordingly. Thanks again to @ehotting for the Gemma / slot save-restore validation. |
|
Additional verification for both #23737 and #19839 on the same hardware. Setup
1) #23737 (speculative MTP /
|
| build | BLOCK_SIZE |
result |
|---|---|---|
| before | default (1 GiB) | ASSERT |
| before | 16 GiB (avoid split) | OK |
| before | 256 MiB (force split) | ASSERT |
| after | 256 MiB (force split) | OK |
2) #19839 (server multi-slot / prompt-cache path)
does not fit my machine spec: Vulkan ErrorOutOfDeviceMemory during context init.
Minimal repro that fits:
GGML_VK_SUBALLOCATION_BLOCK_SIZE=268435456 llama-server -m gpt-oss-120b-mxfp4-00001-of-00003.gguf
-c 1048576 --parallel 4 -ngl 999 --no-mmap
-sps 0 --cache-prompt --cache-idle-slots --cache-ram 8192
--host 127.0.0.1 --port 9091
Then sequential /v1/chat/completions with distinct prompts so LRU rotates slots (-sps 0).
| build | result |
|---|---|
| before | req 1 OK; on req 2 (slot 3 -> 2) ASSERT |
| after | reqs 1–6 OK |
Takeaway
Both issues hit the same assert class: host state IO over KV stream views left without ggml_backend_view_init after a buft max_size split. #23737 enters via speculative MTP checkpointing; #19839 via server multi-slot / idle-slot cache save. This PR fixes that missing view init in the allocator.
|
Additional verification for #21762 on the same hardware. Setup
#21762 (server prompt-cache / 2nd request)Then two independent
Crash is With TakeawaySame missing Fixes #21762 |
Confirmed on b10352 / RADV gfx1151 — this PR is the right fixAdopted this patch locally on Hardware: AMD Ryzen AI Max+ 395 / Radeon 8060S (gfx1151), RADV, Mesa 25.2.8, Vulkan. Unified memory, GTT pool 124 GiB. Model: Qwen3.8-27B UD-Q8_K_XL + F16 mmproj, arch Reproduce (stock b10352, no patch) — llama-server onlyNo extra proxy in front. The abort is in Same flags with
Slot 0 can answer. The next host-copy of slot state kills the process in ~1 s (not DeviceLost, not the 60 s amdgpu watchdog):
This is not OOM. 4×204800 allocates and idles at 86.5 / 124 GiB. RADV Why
|
|
The PR fixed the
Original failure trace: |
When ggml_backend_alloc_ctx_tensors_from_buft splits allocation on buft max_size, a view-only tail at the end of the context could skip the final alloc_tensor_range. Persistent views (e.g. KV k_stream / v_stream) were then left without ggml_backend_view_init. Allocate only parent tensors in alloc_tensor_range and initialize all views in a final pass over the context after all splits complete.
Cover the buft max_size split path where a view-only tail would skip ggml_backend_view_init without the finalize pass.
3f82bd2 to
0074731
Compare
|
Rebased after #27644 updated the base files. Still waiting for review. Thanks. |
When ggml_backend_alloc_ctx_tensors_from_buft splits allocation on buft max_size, a view-only tail at the end of the context could skip the final alloc_tensor_range. Persistent views (e.g. KV k_stream / v_stream) were then left without ggml_backend_view_init.
Allocate only parent tensors in alloc_tensor_range and initialize all views in a final pass over the context after all splits complete.
Overview
ggml_backend_alloc_ctx_tensors_from_buftcan split a context across multiple buffers when a tensor would exceed the backend buffer-typemax_size(for example Vulkan's default 1 GiB). If the remaining tail of the context contains only views, the finalalloc_tensor_rangemay be skipped, so those views never getggml_backend_view_init.Persistent KV stream views (
layer.k_stream/v_stream) then keeptensor->data == NULLwhileview_srcis allocated. Host state save/restore later callsggml_backend_tensor_get/seton those views and hits:GGML_ASSERT(tensor->data != NULL && "tensor not allocated")This change:
Also adds
test_view_init_after_max_size_splitintests/test-alloc.cppto cover the oversized-parent + view-only-tail case.
Additional information
Fixes #19839
Fixes #23737
Fixes #21762
Related:
!tensor->dataguards; complementary discussion)I reproduced the assert with Qwen3.6-27B (MTP) when a parent KV tensor exceeded buft
max_sizeand the context ended with a view-only tail.Note on kv-cache : fix crash in state save/restore #21576: skipping tensors with
!tensor->dataavoids the assert, but can omit real KV still reachable viaview_srcand misalign the serialized state. This PR fixes the missing view init in the allocator instead.Prior investigation commit (same patch on older master; kept for reference):
Yoshi4470@99655d5
Test plan
test-alloc(includes newtest_view_init_after_max_size_split)e3546c794): 0/10 (assert every time)amdvlk64.dllcrash after the assert site was resolved by an AMD driver update; treated as separate from this allocator fixCommand used for the A/B runs:
Requirements