Skip to content

Misc. bug: llama-server Denial Of Service exchaust VRAM #30577

Open

Description

@vlforit

Name and Version

./llama-server --version
version: 0.6.0-dev (build 11646, commit 733f57a)
built with Clang 23.1.3 for FreeBSD amd64

Operating systems

BSD

Which llama.cpp modules do you know to be affected?

llama-server

Command line

llama-server --cache-prompt --chat-template-file /froggeric-chat_template.jinja --host 127.0.0.1 --log-colors on --metrics --min-p 0.0 --no-mmproj-auto --no-host --port 27118 --presence-penalty 0.0 --reasoning-budget -1 --reasoning-preserve --repeat-penalty 1.0 --spec-draft-n-max 3 --n-gpu-layers-draft all --cache-type-k-draft f16 --cache-type-v-draft f16 --spec-type draft-mtp --temperature 1.0 --top-k 20 --top-p 0.95 --no-warmup --alias Qwen3.8-27B-Q4-112 --agent --batch-size 512 --ctx-size 114688 --cont-batching --cache-ram 16384 --cache-type-k f16 --cache-type-v f16 --device Vulkan0 --flash-attn on --fit off --no-kv-unified --load-mode none --model /Qwen3.8-27B-Q4_K_M.gguf --n-gpu-layers -1 --parallel 1 --ubatch-size 512

Problem description & steps to reproduce

After starting the server, open Claude Code and begin chatting. Memory is allocated in 2 MB increments with each prefill, unless doing so would cause an out-of-memory (OOM) condition. My command uses 24,050 MB out of the available 24,525 MB. With the changes introduced in PR #27703, memory is continuously allocated and never released, so it is inevitable that VRAM will eventually be exhausted unless the task is terminated.

@0cc4m & @Nathanw1014 - you introduce DoS into master head.

First Bad Commit

733f57a

Relevant log output

Logs
[48215] 12.26.256.571 E ggml_vulkan: device lost on Vulkan0
[48215] 12.26.256.662 E srv  update_slots: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.26.256.665 E srv    send_error: task id = 10326, error: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.26.256.668 I slot      release: id  0 | task 10326 | stop processing: n_tokens = 61224, truncated = 0
[48215] 12.26.256.672 W srv          stop: cancel task, id_task = 10326
15.07.901.603 I srv  proxy_reques: proxying request to model Qwen3.8-27B-Q4-112 on port 48215
[48215] 12.26.319.858 I slot get_availabl: id  0 | task -1 | selected slot by LCP similarity, f_sim_best = 0.912 (> 0.100 thold), f_keep = 1.000
[48215] 12.26.320.472 I slot launch_slot_: id  0 | task 10330 | processing task, is_child = 0
[48215] 12.26.327.346 E ggml_vulkan: device lost on Vulkan0
[48215] 12.26.327.396 E srv  update_slots: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.26.327.398 E srv    send_error: task id = 10330, error: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.26.327.401 I slot      release: id  0 | task 10330 | stop processing: n_tokens = 61736, truncated = 0
[48215] 12.26.327.406 W srv          stop: cancel task, id_task = 10330
15.08.596.386 I srv  proxy_reques: proxying request to model Qwen3.8-27B-Q4-112 on port 48215
[48215] 12.27.014.812 I slot get_availabl: id  0 | task -1 | selected slot by LCP similarity, f_sim_best = 0.920 (> 0.100 thold), f_keep = 1.000
[48215] 12.27.015.418 I slot launch_slot_: id  0 | task 10333 | processing task, is_child = 0
[48215] 12.27.023.379 E ggml_vulkan: device lost on Vulkan0
[48215] 12.27.023.431 E srv  update_slots: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.27.023.433 E srv    send_error: task id = 10333, error: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.27.023.436 I slot      release: id  0 | task 10333 | stop processing: n_tokens = 62248, truncated = 0
[48215] 12.27.023.440 W srv          stop: cancel task, id_task = 10333
15.09.908.448 I srv  proxy_reques: proxying request to model Qwen3.8-27B-Q4-112 on port 48215
[48215] 12.28.326.986 I slot get_availabl: id  0 | task -1 | selected slot by LCP similarity, f_sim_best = 0.928 (> 0.100 thold), f_keep = 1.000
[48215] 12.28.327.569 I slot launch_slot_: id  0 | task 10336 | processing task, is_child = 0
[48215] 12.28.335.211 E ggml_vulkan: device lost on Vulkan0
[48215] 12.28.335.261 E srv  update_slots: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.28.335.263 E srv    send_error: task id = 10336, error: decode() failed: vk::Queue::submit: ErrorDeviceLost
[48215] 12.28.335.266 I slot      release: id  0 | task 10336 | stop processing: n_tokens = 62760, truncated = 0
[48215] 12.28.335.270 W srv          stop: cancel task, id_task = 10336
^C[48215] 14.34.611.926 I srv    operator(): operator(): cleaning up before exit...
17.16.253.527 I srv    operator(): operator(): cleaning up before exit...

Activity

  1. 0cc4m commented on Oct 15, 2026

    @0cc4m
    Contributor

    It's expected to use more VRAM, it grows with used kv cache. The performance gain is significant though, so you may be able to get to a faster overall speed by reducing VRAM use elsewhere.

  2. vlforit commented on Oct 15, 2026

    @vlforit
    Author

    I don't see how reducing --ctx-size addresses the actual problem. If VRAM keeps growing by ~2 MB on every prefill and is never freed, then OOM is mathematically inevitable for long-running sessions.

    Lowering VRAM usage elsewhere only postpones the failure. It does not prevent it.

    For agentic workloads such as Claude Code, which can run continuously for hours while compacting conversation history, this effectively turns the issue into a delayed denial-of-service condition: sooner or later all available VRAM will be consumed.

    What am I missing here? Is there some mechanism that eventually releases or reuses those allocations? Otherwise, reducing --ctx-size seems like a workaround that merely delays the crash.

  3. 0cc4m commented on Oct 15, 2026

    @0cc4m
    Contributor

    It doesn't grow forever, your kv cache is limited in size.

  4. vlforit commented on Oct 15, 2026

    @vlforit
    Author

    I understand that the KV cache itself is bounded by --ctx-size and therefore VRAM growth is not literally infinite.

    However, my concern is different.

    I reduced the context size and the initial allocation dropped to about 22,940 MB out of 24,525 MB available. Despite that, I still hit an OOM during this discussion because of the additional allocations introduced by this PR.

    From a user's perspective, this makes VRAM usage difficult to predict. Previously, I could estimate whether a configuration would fit based on the reported allocation at startup. After this change, there is an additional amount of VRAM consumed dynamically during runtime, but it is not obvious how much will eventually be required.

    As a result, the only practical way to determine whether a configuration is safe is to keep reducing parameters such as --ctx-size and repeatedly test until OOMs stop occurring.

    That's why I believe the current design is problematic. The issue is not that memory grows forever. The issue is that the runtime VRAM overhead is effectively hidden from the user, making capacity planning much harder.

    Users need a way to understand and estimate the worst-case VRAM usage. Otherwise, a configuration that appears to fit at startup may still fail later under normal workloads, especially with long-running agentic tools such as Claude Code.

  5. ggerganov commented on Oct 15, 2026

    @ggerganov
    Member

    Ideally, the worst case memory should indeed be allocated at the start. However, the usage of dynamic memory pools in some cases breaks this contract. The CUDA backend also has a similar shortcoming.

    In the Metal backend we solved this by allocating extra memory in the compute graph by overloading the get_alloc_size of the buffer types.

    This was introduced here: #15966

    We now use it for a few ops that require extra allocations:

    static size_t ggml_backend_metal_buffer_type_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) {
    size_t res = ggml_nbytes(tensor);
    // some operations require additional memory for fleeting data:
    switch (tensor->op) {
    case GGML_OP_MUL_MAT_ID:
    {
    res += ggml_metal_op_mul_mat_id_extra_tpe(tensor);
    res += ggml_metal_op_mul_mat_id_extra_ids(tensor);
    res += ggml_metal_op_mul_mat_id_extra_amax(tensor);
    } break;
    case GGML_OP_FLASH_ATTN_EXT:
    {
    res += ggml_metal_op_flash_attn_ext_extra_pad(tensor);
    res += ggml_metal_op_flash_attn_ext_extra_blk(tensor);
    res += ggml_metal_op_flash_attn_ext_extra_tmp(tensor);
    res += ggml_metal_op_flash_attn_ext_extra_kv_f16(tensor);
    res += ggml_metal_op_flash_attn_ext_extra_idx(tensor);
    } break;
    case GGML_OP_CUMSUM:
    case GGML_OP_ARGSORT:
    {
    res *= 2;
    } break;
    case GGML_OP_TOP_K:
    {
    res = 2*sizeof(int32_t)*ggml_nelements(tensor->src[0]);
    } break;
    default:
    break;
    }
    return res;
    GGML_UNUSED(buft);
    }

  6. 0cc4m commented on Oct 15, 2026

    @0cc4m
    Contributor

    Yes, it would make sense to do it that way instead of the current internal buffers. I'll look into it.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions