FP16 KV cache with packed half2 split-KV attention#138
Open
orionpapadakis wants to merge 5 commits into
Open
Conversation
With -Dllama.kvcache.fp16=true (and the NVIDIA scheduler path), LlamaState and Qwen3State additionally allocate half-precision KV caches, and the standard single-token task graphs read/write those instead of the FP32 ones, halving decode KV-cache bandwidth. Requires TornadoVM packed half2 support (HalfFloatArray.getHalf2/setHalf2, Half2 conversion helpers). - Llama: ropeRotationWithCacheCopyFP16 packs the rotated K pair and the V pair with single 32-bit stores; processHeadsFlashAttentionFP16 reads the K/V tiles with packed half2 loads and expands to FP32 shared-memory tiles. All softmax/output arithmetic stays FP32. - Qwen3: ropeRotationWithCacheCopyFP16 (scalar half stores; Qwen3 rotation pairs sit a half-head apart, so pairs are not adjacent on the write side) and processHeadsFlashAttentionSplitKVFP16 (packed reads, FP32 online-softmax accumulation; combine phase unchanged). - processHeadsFlashAttentionFP16Scalar plus -Dllama.kvcache.fp16.scalar isolate the packed-load gain from the halved-footprint gain when benchmarking. - The prefill/decode graph variants share the KV cache with the batch prefill layers, so they override useFp16KVCache() to false; the flag currently applies to standard (single-token) mode only. FP32 buffers stay allocated, so all other paths are unaffected.
Route the persistOnDevice/consumeFromDevice calls in the decode and batch-decode graphs to the FP16 key/value buffers when the FP16 KV cache is active, so the flag-gated cache added for single-token decode is also honoured on the prefill-decode and batch paths.
Introduce a -Dllama.attention.deepHalf2 path for the FP16 KV cache: stage Q once per workgroup as a __half2 local-memory tile and accumulate each K/V pair with a single __hfma2, converting to FP32 once per row (llama.cpp fattn-vec style) instead of per pair. Adds the packed single-token and batch-prefill attention kernels for Llama and Qwen3, and makes the split-KV count configurable via -Dllama.attention.splitKv.count. Requires the packed half2 local-array API (allocateHalf2LocalArray), so bump the TornadoVM dependency to 5.1.1-jdk21-dev.
Optionally keep decoding past the stop token so benchmark runs generate a fixed token count, making before/after throughput directly comparable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds an opt-in half-precision (FP16) KV cache for the NVIDIA/CUDA decode path,
halving KV-cache bandwidth while keeping accumulation in FP32. On top of it, a
packed-
half2split-KV attention path ("deep-half2") stages the query as a__half2local-memory tile and consumes each K/V pair with a single__hfma2,converting to FP32 once per row (llama.cpp
fattn-vecstyle) instead of per pair.Everything is flag-gated and additive: the FP32 KV cache remains the default and
the fallback for non-NVIDIA backends and unsupported models, so existing behaviour
is unchanged unless a flag is set.
Scope
half2codegen inthe CUDA backend; other backends transparently keep the FP32 cache.
Flags
-Dllama.kvcache.fp16=true-Dllama.kvcache.fp16.scalar=truehalf2(isolates the packed-load gain).-Dllama.attention.deepHalf2=true__hfma2); requires the FP16 cache.-Dllama.attention.splitKv.count=N-Dllama.bench.ignoreEos=trueDependency
The deep-half2 path uses the packed
half2local-memory API(
allocateHalf2LocalArray,Half2.fma/lowFloat/highFloat) added in theTornadoVM
feat/packed-half2branch, sopom.xmltargetstornadovm.base.version = 5.1.1-jdk21-dev.This PR does not build against a released TornadoVM until that branch is merged
and published. Please treat as dependent on the corresponding TornadoVM PR.
Testing
Built with JDK 21 against a local TornadoVM
5.1.1-jdk21-dev(CUDA backend) andrun on an RTX 5090 (Linux).
Baseline = the implementation before FP16 KV / half2. Enable the path with:
Output remains coherent with the flags enabled; FP32 remains the default when they
are not set.
Throughput
Standard single-token decode path (no prefill/decode), 2048 tokens per run at equal
length (
-Dllama.bench.ignoreEos=true), fixed seed and prompt, RTX 5090 Laptop.Baseline = same binary with the flags unset (FP32 KV cache). Decode throughput (tok/s):
-Dllama.kvcache.fp16+ -Dllama.attention.deepHalf2there, so halving it helps directly.
token (QK-norm, always-on split-KV attention), so the KV-cache byte share is smaller;
0.6B is essentially launch-overhead-bound.
this depth — slightly below on Llama, slightly above on Qwen3 — so it is kept as an
opt-in refinement rather than folded into the default FP16-KV path.
The batched-prefill/decode path (
--with-prefill-decode --batch-prefill-size 32,decode-phase tok/s) shows the same behaviour:
-Dllama.kvcache.fp16+ -Dllama.attention.deepHalf2(Qwen3-4B batched needs a larger device-memory budget, e.g.
--gpu-memory 22GB, thanthe 14 GB default; the standard-path run fits in the default.)