Skip to content

KV offload: defect register, measurements and roadmap - #31

Closed
Piggidragon wants to merge 8 commits into
GenerelSchwerz:beellama/devfrom
Piggidragon:beellama/kv-host-decode-transport
Closed

Piggidragon wants to merge 8 commits into
GenerelSchwerz:beellama/devfrom
Piggidragon:beellama/kv-host-decode-transport

Conversation

@Piggidragon

@Piggidragon Piggidragon commented Aug 24, 2026 •

Copy link
Copy Markdown

This branch contains no code change. It is a measurement ground and a plan,
rebuilt from beellama/dev at 946c1e5b6 with a clean tree and a clean build.

It started as a search for new decode optimizations for a host-resident KV cache.

Current status update

  • R1 / D2: the divergent CUDA FlashAttention barrier is independently reproduced on unmodified current upstream and reported as ggml-org/llama.cpp#27678. An upstream fix must be merged, incorporated here, and revalidated before R1 is closed.
  • D3: the MTP + host-resident-KV abort remains confirmed, but D2 is only a correlated upstream defect. It is not proven to cause D3, so the owner and mechanism remain open.
  • R9 / D7: PR #34 implements the two-line KVarN build repair. It is pending merge and a clean enabled-KVarN build on beellama/dev; D7 remains historical evidence for this PR's base commit, not a current-dev claim after fix: restore CUDA KVarN build #34 lands.
  • R7: deliberately not planned. Making default-build validation cover this KVarN path would add unacceptable CUDA build time; run it only in an explicitly enabled KVarN build when needed.

The optimizations were built, measured, and all but one rejected — and the
measurements falsified two conclusions this lane had already recorded. What is
worth keeping is the evidence and the plan, so that is what this PR is.

docs/kv-offload-roadmap.md Twelve items: owner, size, blast radius, blockers, acceptance gates
docs/kv-offload-defects.md Ten defects, each with the exact command and the exact failing output
docs/kv-offload-measurements.md Every number, and what each one rules out
docs/repro/ One script per defect and per measurement, with how each was verified
docs/probes/ Six patches, verified to apply to pristine 946c1e5b6

Defects, re-confirmed on this base

# Defect Status Owner
D1 Head dim 320 routed to a vector kernel that has no D320 instance confirmed, exit 134 at fattn.cu:412 BeeLlama
D2 Divergent __syncthreads() in the MMA FA kernel confirmed, localized, and reported upstream as #27678 upstream
D3 MTP + host-resident KV aborts mid-generation confirmed, 1 in 3 runs, twice independently; cause not established investigation; D2 correlation only
D4 test-kvarn aborts on any default build with a CUDA device confirmed; not planned BeeLlama
D5 Host-resident KVarN: CPU fallback, 108x the host workspace confirmed, both arms re-measured BeeLlama + us
D6 Quantized K/V + logit_softcap not reproduced — coverage is one case wide BeeLlama
D7 GGML_CUDA_KVARN=ON does not compile historical base failure; fixed by #34 pending merge/validation BeeLlama
D8 q2_0s FlashAttention returns wrong values at hsk=64 confirmed, one case fails at three seeds BeeLlama
D9 KVarN is refused for a device-resident cache on a default build confirmed BeeLlama
D10 test-upstream-merge-keepers-static fails on a clean checkout confirmed BeeLlama

D6 is listed precisely because it could not be confirmed. PR #4 records it; the
suite enumerates logit_softcap != 0 only at hsk == 128 and has exactly one
quantized softcap case, which passes.

D1 is broader than recorded. PR #4 gave the confirmed reproducer as GQA 32;
on 946c1e5b6 the suite aborts at nr23=[4,1], kv=512, nb=75.

D7: historical KVarN-build failure; PR #34 supplies the repair

On this PR's base, GGML_CUDA_KVARN=ON did not compile at all — 82 errors, all no instance of function template flash_attn_ext_f16_process_tile at fattn-mma-kvarn-case.cuh(178).

flash_attn_ext_f16_process_tile gained a parameter,
causal_prefix_first_bound_smem, in 0a85856ee. The non-KVarN caller passes it;
both KVarN call sites, at :178 and :245, still pass the old argument list.
git blame attributes the signature change and both stale call sites to the
same author.

The fix is two lines, one nullptr each, and it is verified rather than
asserted: with probes/06 the build
completes with exit 0 and zero errors, without it 82. PR #34
carries that repair. It remains pending merge and validation on beellama/dev.

D2, localized

compute-sanitizer --tool synccheck --show-backtrace device on a pristine
-lineinfo build gives 100 barrier errors, all at one line:

Barrier error detected. Divergent thread(s) in block.
  at flash_attn_ext_f16_process_tile<256,256,4,8,4,0,0,0,1,F16,F16>
     in fattn-mma-f16.cuh:1732
if (np > 1 && threadIdx.y % np == 0) {   // 1687
    __syncthreads();                     // 1732  <-- reported
} else if (np > 1) {                     // 1764
    __syncthreads();                     // 1768  balancing barrier
}

np == nwarps == 4, so only threadIdx.y == 0 takes the if. Balancing the
count is necessary but not sufficient
— __syncthreads() is defined only where
its guard evaluates identically across the block, and threadIdx.y % np == 0
does not. git blame puts both lines on upstream's CUDA maintainer
(0208355f42, 95e18884fc); the fork owns neither.

This corrects an earlier note in our own record, which said the balancer "is
present and correct" and concluded the divergence was elsewhere. The first half
was right and the conclusion was wrong.

The measurement that changes the plan

The decode token is copy + compute in series. Two independent routes agree
to within 4%: the telemetry's blocking H2D, and subtracting a device-resident arm.

Depth host KV ms/tok device KV ms/tok implied copy measured H2D agreement
4,096 31.72 31.53 39.5148 25.31 6.22 ms 6.31 ms 1.4%
16,384 19.69 50.79 36.2507 27.59 23.20 ms 24.05 ms 3.5%
32,768 13.03 76.75 32.7283 30.55 46.19 ms 47.10 ms 1.9%

Remove the ordering constraint — deliver on a copy stream nothing waits on,
incorrect output, upper bound only — and the copy engine hides 96–99% of it:

Depth ordered unordered gain share of max(copy,compute)
4,096 31.7287, 31.7165 38.4860, 38.5025 +21.3% 98.6%
16,384 19.7025, 19.6820 34.8800, 34.8439 +77.0% 96.5%
32,768 13.0142, 13.0137 20.8258, 20.8300 +60.0% 95.9%

The penalty is the ordering, not the bandwidth. Transfers already run at the
practical PCIe 4.0 x16 rate.

Two things this lane had wrong

  • The store side is not the serializer. Removing all 32 blocking D2H copies,
    all 32 CPU set_rows nodes and 32 of 33 splits per token is worth +0.18%
    at 32,768 and nothing at 4,096. The store moves 34.8 KB per token, not the
    2.2 MB an earlier revision recorded — wrong by ~60x, and that error is what
    made the store look like a plausible culprit.
  • The transfer can be hidden behind compute. Two earlier attempts concluded
    it could not. Both were argued from structure; both fell to measurement.

Rejected with evidence, each in measurements §5:
async H2D on the compute stream (+0.13%), cross-layer prefetch (+1.38% and not
exact), write-combined pinned memory (−0.85%), huge pages (null result, IOMMU
premise absent on this host), NUMA (untestable, one socket).

Roadmap

# Item Owner Size Blast radius Blocked by
R1 Fix the divergent __syncthreads() upstream M–L very high —
R2 Fix D320 vector routing BeeLlama S–M medium —
R3 Interim guard for MTP + -nkvo this fork S–M medium —
R4 Correct copy/compute overlap this fork XL high design decision
R5 Re-measure the vector/MMA crossover this fork S low R1
R6 KVarN in the KV-offload line BeeLlama + us XL high R9, ownership decision
R7 test-kvarn on default builds BeeLlama — — not planned: unacceptable default CUDA build time
R8 Widen quantized × softcap coverage BeeLlama S none —
R9 Fix the GGML_CUDA_KVARN=ON build BeeLlama S none implemented in #34; pending merge and validation
R10 Fix q2_0s FlashAttention at hsk=64 BeeLlama S-M medium —
R11 Make KVarN reachable on a device-resident cache BeeLlama M high PR #34 merge and validation
R12 Repair test-upstream-merge-keepers-static BeeLlama S none —

Order. R1 first: it gates the two largest numbers on record — MTP on a
host-resident cache is +75.9% on a 24,000-token prompt where it completes, and
PR #4's vector/MMA crossover is +11.59% on decode at 32,768 and cannot be taken
while lowering the threshold routes more work onto the diverging shapes. It is a
defect fix, not a feature, and it belongs upstream.

Then merge and validate R9 / PR #34. R7 is deliberately not planned because of default CUDA build time. Then the other small BeeLlama items. Then R4.

Two decisions are needed before anyone writes code. Where the staging ring
for R4 lives — fork-local or a generalization of upstream's n_copies
machinery. And who implements KVarN host-residency, which is a conversation
between us and BeeLlama rather than a technical question.

Supersedes PR #32

That PR added a fail-closed guard for KVarN + --no-kv-offload. The guard works
— verified at runtime, it refuses with a named error instead of degrading — but
it converts a slow configuration into an error message, which is not a fix and
gets in the way of the real one. The analysis is folded into D5 and the
integration is roadmap item R6. #32 is closed.

Validation

RTX 4070 (12 GB, sm_89, CUDA 13.3), i5-13400F, one socket, no IOMMU. All
processes under taskset -c 0,2,4, one GPU job at a time behind a lock —
concurrent jobs produced a CUDA OOM and an arm with σ = 1.72 against a normal
σ ≈ 0.06; both were discarded and re-run. Model Qwen3.8-27B-UD-IQ2_M.gguf,
q8_0/q8_0, -b 512 -ub 512 --parallel 1. llama-bench arms are --no-warmup
with reverse-order A/B/A/B pairs; server arms are greedy, seed 1234.

Every d* script was executed end to end and the output in the defect register
is what it printed. The m* scripts are tidied-up forms of the ad-hoc commands
that produced the measurements; m4 has since been run against build-clean,
m1–m3 are verified for argument handling but have not regenerated their
numbers. docs/repro/README.md states this per script.

@Piggidragon
Piggidragon force-pushed the beellama/kv-host-decode-transport branch from 460bf8b to bc549ee Compare August 24, 2026 18:21
This branch carries no code change. It is a measurement ground and a plan,
built fresh from beellama/dev at 946c1e5.

Every defect was re-confirmed on that base with a clean build tree, or is
marked as not confirmed. Four of six reproduce from the scripts committed here;
D5 needs a GGML_CUDA_KVARN=ON tree and its numbers are labelled as carried
forward; D6 could not be reproduced at all and says so.

docs/kv-offload-defects.md -- six entries, each with the exact command, the
exact failing output, and the owning layer:

  D1 head dimension 320 routed to a vector kernel that has no D320 instance,
     aborting at fattn.cu:412. Reachable at nr23=[4,1], which is a lower ratio
     than PR #4 recorded, so the documented reproducer was narrower than the
     behaviour. Owner: BeeLlama.
  D2 divergent __syncthreads() localized to fattn-mma-f16.cuh:1732, 100 of 100
     synccheck errors at one line, both lines blamed to upstream. Balancing the
     barrier count is necessary but not sufficient: the guard
     threadIdx.y % np == 0 is thread-dependent, so both barriers sit in
     divergent code. Owner: upstream.
  D3 MTP with a host-resident cache aborts with an illegal memory access,
     1 in 3 runs, reproduced twice independently.
  D4 test-kvarn aborts on any default build with a CUDA device present, so the
     validation command CLAUDE.md documents cannot pass as written.
     Owner: BeeLlama.
  D5 host-resident KVarN falls to the CPU backend and reserves ~0.099 MiB of
     host workspace per context token, 25.9 GiB at 262,144. Not re-run here.
  D6 quantized K/V with logit_softcap: NOT reproduced. The suite carries
     exactly one quantized softcap case and it passes.

docs/kv-offload-measurements.md -- the numbers and what they rule out. The
decode token is copy + compute in series, confirmed two independent ways to
within 4%. Removing the entire store side is worth 0.18%. Removing the ordering
constraint reaches 96-99% of max(copy, compute): +21.3% / +77.0% / +60.0% at
4,096 / 16,384 / 32,768. Bytes, the store side and allocation properties are all
ruled out as levers; ordering is the lever.

docs/kv-offload-roadmap.md -- eight items with owner, size, blast radius,
blocking relationships and acceptance gates, plus the two decisions that must be
made before anyone writes code: where the staging ring lives, and who implements
KVarN host-residency.

docs/repro/ -- one script per defect and per measurement, all syntax-checked and
four of them executed end to end for this commit. They serialize on a lock
because concurrent GPU jobs produce failures that look like defects.

docs/probes/ -- the five measurement patches, verified to apply to pristine
946c1e5 and to revert without residue. Three produce incorrect output by
construction and say so. None is a candidate implementation.

The KVarN fail-closed guard from the earlier branch is deliberately not here. It
converts a slow configuration into an error message, which is not a fix and gets
in the way of the real one; the integration is roadmap item R6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8bBMKTLSzVbDBSUfdQc9N
@Piggidragon
Piggidragon force-pushed the beellama/kv-host-decode-transport branch from 2a491ff to c602c83 Compare August 24, 2026 20:43
@Piggidragon Piggidragon changed the title Host-resident KV decode: transport telemetry, rejected overlap, accepted width lever KV offload: defect register, measurements and roadmap Aug 24, 2026
Piggidragon and others added 6 commits August 24, 2026 22:44
The synccheck run that localized the divergent barrier was taken on a -lineinfo
tree carrying the transport-telemetry patch, which touches ggml-cuda.cu and not
fattn-mma-f16.cuh and is inert unless its environment variable is set, so the
kernel under test is bit-identical to a pristine build. Says so rather than
implying the run came from a pristine tree, and names the re-run as the one
verification step still outstanding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8bBMKTLSzVbDBSUfdQc9N
Wiped build-li, reconfigured from scratch at 946c1e5 with -lineinfo only, and
re-ran docs/repro/d2-barrier.sh. Identical result: 100 barrier errors, all at
fattn-mma-f16.cuh:1732, the same two fixup instantiations of
flash_attn_ext_f16_process_tile<256,256,4,8,4,...,F16,F16>, the same call frame
at :2104. The request also aborted under the sanitizer, which is D3 firing.

Removes the provenance caveat: the localization no longer rests on a tree that
carried any patch, and every entry in the register except D5 is now reproduced
from the committed scripts against a clean build of the current dev head.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8bBMKTLSzVbDBSUfdQc9N
…em R9

Found while trying to re-measure D5: the KVarN configuration cannot be built on
946c1e5 at all. 82 compile errors, all "no instance of function template
flash_attn_ext_f16_process_tile", at fattn-mma-kvarn-case.cuh(178).

flash_attn_ext_f16_process_tile gained a parameter, causal_prefix_first_bound_smem,
immediately after compact_causal_prefix in 0a85856. The non-KVarN caller at
fattn-mma-f16.cuh:2105 passes it; both KVarN call sites, at
fattn-mma-kvarn-case.cuh:178 and :245, still pass the old argument list. git
blame attributes the signature change and both stale call sites to the same
author, so this is BeeLlama's on both ends.

The fix is two lines, one nullptr per call site. Included as
probes/06-kvarn-build-fix.patch and verified: with it the build completes with
exit 0 and zero errors, without it 82. It is a reference, not a proposal -- per
the shape of this branch it should land as its own change, which is roadmap
item R9.

This also explains and strengthens D5's status. D5 was previously "not re-run";
the correct statement is that nobody can re-run it as the tree stands. With D7
patched, the kvarn5 arm was re-measured on a fresh build and reproduces the
carried-forward figure exactly: 2,195.90 MiB of host compute buffer at -c 8192
against 122.01 MiB on the device.

Also fixes docs/repro/d5-kvarn-workspace.sh, which emitted a 466 MB log because
GGML_SCHED_DEBUG=2 and -v were on by default. The node breakdown is now behind
LLAMA_KV_SCHED_DEBUG=1 with a warning about the size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8bBMKTLSzVbDBSUfdQc9N
The four measurement scripts defaulted their depth list with
`for D in "${@:-4096 16384 32768}"`. The quotes make the default a single
word, so the no-argument form -- the one the README documents -- passed all
three depths as one field. m1 and m4 interpolated it into `-d $D`, which
word-split into `-d 4096 16384 32768`; llama-bench rejected the trailing
values as unknown arguments and exited 1 before measuring anything. m2 and m3
passed the fields as separate positional arguments to `run`, so the second
depth landed in `-r`: 16384 (m3) and 32768 (m2) benchmark repetitions at a
single depth instead of 3 at three depths, and `[ "$D" -le 4096 ]` failed with
"integer expression expected". Use a real default array instead.

d5 reported `exit=$?` after a pipeline ending in `head`, so the non-sched
branch always printed 0 even when llama-cli failed to load the model -- which
is the failure the script exists to surface, since D5 needs a
GGML_CUDA_KVARN=ON build that does not compile without probe 06. Record
PIPESTATUS[0] instead. Also carries the pending -v and filter-width edits to
the same branch.

Verified with stubbed llama-bench/llama-cli: m1 now issues six runs over the
three depths, m2/m3 issue -r 5 at 4096 and -r 3 above it, m4 issues three runs
at -r 3, explicit-argument invocation is unaffected, and d5 reports the
child's status in both branches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vezsq8VRiPzENCmhCg8q41
With the D7 patch applied so the configuration compiles at all, both arms of D5
were measured at -c 8192 host-resident on a fresh GGML_CUDA_KVARN=ON build:

  kvarn5   CUDA0 122.01 MiB   CUDA_Host 2,195.90 MiB
  q8_0     CUDA0 142.27 MiB   CUDA_Host    20.28 MiB

Both reproduce the carried-forward figures exactly -- 108x more host workspace
for the cache that is supposed to be the smaller one. The device side moves the
other way, kvarn5 reserving less on the GPU than q8_0, which is the tell that
the work moved to the host rather than being avoided.

Only the context-scaling table and the per-node breakdown remain from an earlier
session, and both are now labelled as such at the point of use.

Also fixes docs/repro/d5-kvarn-workspace.sh a second time: the previous fix
dropped -v, but the sched_reserve lines that report the compute buffers are
verbose-only, so the script filtered away exactly what it was supposed to
capture. GGML_SCHED_DEBUG is what makes the output enormous, not -v.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8bBMKTLSzVbDBSUfdQc9N
Two corrections, both about overstating validation.

First, docs/repro/README.md listed all scripts as if equally verified. They were
not: every d* script was executed end to end and the output in the defect
register is what it printed, but the m* scripts were only syntax-checked before
being committed, which is exactly how the argument-handling bug in bba4e10
survived -- `bash -n` does not catch `"${@:-a b c}"`. The table now carries a
"verified how" column and says plainly that the m* scripts are tidied-up forms
of the ad-hoc commands that produced the numbers, not the commands themselves.

Second, m4 has now been run against build-clean rather than assumed:

  4,096   39.5148 +- 0.1561
  16,384  36.2507 +- 0.1053
  32,768  32.7283 +- 0.0958

against the recorded 39.0152 / 36.1072 / 32.5832, which is 0.4-1.3% lower. The
measurement table quotes the fresh figures, keeps the earlier ones in a note,
and states the effect: the implied copy term moves by at most 0.33 ms and the
agreement with the independently measured blocking H2D improves from 6.6/4.0/2.2%
to 1.4/3.5/1.9%. Both sets support the same conclusion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8bBMKTLSzVbDBSUfdQc9N
@Piggidragon
Piggidragon marked this pull request as ready for review August 24, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant