Skip to content

ggml-meta : bound the stack use of the split state walk - #10

Open
Piggidragon wants to merge 1 commit into
llama/devfrom
ggml/meta-split-state-stack
Open

Piggidragon wants to merge 1 commit into
llama/devfrom
ggml/meta-split-state-stack

Conversation

@Piggidragon

Copy link
Copy Markdown
Owner

Overview

ggml_backend_meta_get_split_state derives the split state of a tensor from the split states of its
sources, by calling itself for every source. One frame of that function measures about 42 kB, since
it holds several copies of ggml_backend_meta_split_state, which is ~2 kB wide because ne covers
16 segments per device.

A cache normally keeps that recursion one level deep: the allocator walks the graph in topological
order, so the sources of a node are already cached when the node is reached. But a stale entry
clears the whole cache, not only that entry. When that happens at a late node, every source is gone
and the node derives the full chain back to the inputs again. On a 65 layer graph that is about 1560
frames, ~66 MB of stack, and the process dies on the default 8 MiB thread stack.

Fill the sources from the leaves up first, with an iterative walk, so the recursion finds them
cached and stays one level deep. Two details matter:

  • A stale entry would clear the cache in the middle of the walk and drop what the walk just filled.
    The walk therefore validates an entry itself and clears up front. The cache is empty afterwards,
    and filling an empty cache cannot clear again, which bounds the walk to two attempts.
  • The walk does not descend into a source that is already cached, otherwise it would cost
    O(graph) per node.

The walk reaches what the recursion reaches: a leaf carries its own state and has no sources, and
the ggml_nelements() == 0 shortcut is kept. No tensor is queried that was not queried before, so
no new assert becomes reachable.

The same code is in upstream master unchanged, so it is affected as well.

Testing

65 layer hybrid model, --split-mode tensor over two devices, host resident KV cache, DFlash2
speculative decoding. A short text conversation followed by a prompt with an image, reproduced
several times:

  • before: SIGSEGV in ggml_backend_meta_get_split_state, 197 recursion frames on the stack, the
    faulting stack pointer below the stack mapping, peak stack 66.5 MiB
  • after: answers normally on the default 8 MiB stack, peak stack 176 kB

Equivalence of the derived states, from GGML_META_DEBUG=1 on both builds: the two runs agree on
all 7607 shared keys of source states, name, op and per device shapes, including the 135 keys that
carry two states for assume_sync true and false. No key differs.

test-alloc: 42 passed, exit 0.

Throughput on the same image prompt: prompt eval 27.59 -> 27.63 ms per token, eval
41.10 -> 40.82 ms per token.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES - diagnosed and implemented by an agent on my instruction, see the commit trailer.

🤖 Generated with Claude Code

get_split_state recurses into the sources of a tensor. A stale entry clears
the whole cache, so a late node derives the full chain again, one large frame
per node: a 65 layer graph needs ~66 MB of stack and dies on the default 8 MiB.

Fill the sources from the leaves up first, iteratively, so the recursion finds
them cached and stays one level deep.

Assisted-by: Claude Opus 5
@github-actions github-actions Bot added the ggml label Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant