~/blog/gtx-970-gemma4-e2b-kv-cache-flash-attention

Gemma 4 on a GTX 970 · part 3

[趣味競賽] 在 GTX 970 上,Flash Attention 讓長 context 的 decode 接近翻倍(24.3 → 42.5 tok/s)

cat --toc

TL;DR

在一張 2014 年的 GTX 970 上(Maxwell、沒有 tensor core),跑 Gemma 4 E2B、餵一條 7719 token 的 prompt、-c 16384,一般的 KV cache 常識整個翻過來。開 Flash Attention 讓長 context 的 decode 接近翻倍:24.3 → 42.5 tok/s(+75%,KV 都固定 f16),跟「老卡開 FA 沒用、甚至會傷」的一般印象完全相反。FA 還順便省了約 430MB VRAM(3856 → 3427 MiB)——不開 FA,這張 4GB 卡跑 16K context 只剩 175MB free。而大家想省記憶體第一個會去動的 q8 KV cache,在這顆上是個壞主意:只省了約 15MB(因為 sliding-window attention 早就把 KV 壓得很小),還把 decode 拖到 32.3 tok/s,因為 Maxwell 沒有快速 int8。我把 production 改回了 f16 KV。decode 數字跑兩次都穩;prefill 我沒測到乾淨的數字,所以不寫。

白話版:老卡上的兩個直覺,全都反了

跑 LLM 在一張小小的老卡上,大家「都知道」兩件事:Flash Attention 要關掉(那是 tensor core 的東西,Maxwell 用不到),KV cache 量化要開(VRAM 小,當然要把 cache 壓一壓)。我在一張 2014 年的 GTX 970 上跑今年的 Gemma 模型,這兩件事全都反過來。開 Flash Attention,長一點的回答快了將近一倍,還順手多出一塊記憶體。把 cache 壓成 q8,幾乎沒省到,反而拖慢。這篇就是在講:為什麼這些老掉牙的判斷,在這張卡上會指錯方向。


這篇接在哪

Part 1 發現沒有 tensor core 的卡卡在解量化,所以最簡單的量化格式贏。Part 2 再接上視覺、音訊、TTS,把它變成一個完整的離線助手。這兩篇都跑在短 context。這篇把它拉長:一條 7719 token 的 prompt、16K 的 context window,還有你直覺會想去動的兩個旋鈕——Flash Attention 跟 KV cache 量化。

我一開始是帶著一個一般印象進去的:Flash Attention 是 tensor core 的把戲,在 2014 年的 Maxwell 卡上不會有用,搞不好還會傷。這個印象是錯的,而它怎麼錯的,就是整篇文章。

設定:一條長 prompt,兩個旋鈕

機器跟系列裡其他篇一樣:GTX 970(Maxwell、compute capability 5.2、沒有 tensor core、可用約 3.5GB)、Gemma 4 E2B QAT Q4_0(約 3.2GB)、llama.cpp 編成 sm_52。這顆模型用 sliding-window attention(SWA),n_swa=512——記住這個數字,一半的常識就是栽在它身上。

我餵同一條 7719 token 的 prompt、16K context window,調兩個參數:Flash Attention(-fa),跟 KV cache 的精度(--cache-type-k/--cache-type-v)。

# FA 關、f16 KV(「老卡 → FA 關」的預設想法)
./build/bin/llama-server \
  -m ~/models/gemma-4-E2B-QAT-Q4_0.gguf \
  -ngl 99 -c 16384 --port 8080

# FA 開、f16 KV
./build/bin/llama-server \
  -m ~/models/gemma-4-E2B-QAT-Q4_0.gguf \
  -ngl 99 -c 16384 -fa --port 8080

# FA 開、q8 KV(「VRAM 小 → 把 cache 壓一壓」的反射動作)
./build/bin/llama-server \
  -m ~/models/gemma-4-E2B-QAT-Q4_0.gguf \
  -ngl 99 -c 16384 -fa \
  --cache-type-k q8_0 --cache-type-v q8_0 --port 8080

三組設定、一條長 prompt,每組 decode 都量兩次確認穩。

Flash Attention 讓長 context 的 decode 接近翻倍

三組跑出來是這樣,要看的是長 prompt 的 decode 那欄:

設定KV長 prompt decodeVRAM used (free)
FA f1624.3 tok/s3856 MiB(剩 175
FA f1642.5 tok/s3427 MiB(剩 605)
FA q8_032.3 tok/s3412 MiB(剩 620)

GTX 970 上的長 context decode:Flash Attention 關 24.3 tok/s、FA 開(f16 KV)42.5、FA 開搭 q8 KV 32.3——FA 讓 decode 接近翻倍,q8 KV 反而拖慢。

KV 都固定 f16,光是把 Flash Attention 打開,decode 就從 24.3 衝到 42.5 tok/s——多了 75%,接近翻倍。短 context 的 decode 三組都落在 42–47 tok/s 附近,所以差距只有在 prompt 變長之後才拉開。decode 數字跑得很穩:f16 那組兩次是 42.86 跟 42.48,q8 兩次是 32.37 跟 32.34。這不是雜訊。

這跟我一開始的預期完全相反。Flash Attention 老是跟 tensor core 綁在一起講,所以大家的鄉野傳說是:在 Maxwell 卡上它沒什麼用,乾脆關掉。結果在這張卡上,關掉它害我長 prompt 的 decode 少掉 43%。

老實講,prefill/TTFT 我也順手記了,但都是單次、而且跨設定跳得很怪、不合常理。我沒跑出跨多輪乾淨的 prefill 數字,所以這篇就不寫——這篇講的是 decode,那才是跑兩次都穩的部分。

為什麼在沒有 tensor core 的卡上,FA 還能贏 decode

原因出在 decode 這一步到底在做什麼。生一個 token 是一次單 token 的 forward,貴的不是矩陣運算,是把整塊 KV cache 在記憶體裡搬一遍、拿去算 attention。context 越長,每個 token 要掃的 KV 就越多。

一般的 attention 會把整張 attention 分數矩陣攤在記憶體裡、再讀回來。Flash Attention 把整段 attention 計算 fuse 成一趟、根本不把那張分數矩陣寫出來——它分塊(tile)掃過 KV、邊掃邊累加。長 prompt 下,這代表每生一個 token 的記憶體搬運量小很多,而那正好是 decode 卡住的地方。所以 FA 贏,而且 context 越長贏越多——這也是為什麼短 prompt 看起來都一樣、7719 token 那條才拉開。

重點其實不在 tensor core。FA 在這張卡上贏 decode,不是因為 matmul 變快,是因為它不用每一步都把一張大分數矩陣搬進搬出記憶體。一張沒有 tensor core 的卡,照樣能從「少搬資料」得到好處。

FA 還省了約 430MB——不開它,16K context 根本塞不下

再看一次 VRAM 那欄。FA 開(f16)用了 3427 MiB,FA 關是 3856 MiB——同樣 KV 精度,省了約 430MB。這跟機制對得起來:不攤開那張分數矩陣,就不用配那塊記憶體。

而這 430MB 在一張 4GB 卡上是會出事的。FA 的時候,16K context 那一跑只剩 175MB free。這種餘量,桌面一重繪、或哪裡偷配一塊記憶體,就把你推進 OOM 了。在這張卡上,Flash Attention 不是「速度的加分項」——它是讓一張 4GB 卡能撐住 16K context 的前提。所以 FA 是 decode 贏、VRAM 也贏,不只是省記憶體的小技巧。

q8 KV cache:省了約 15MB,賠掉 24% 的 decode

再來看第二個直覺操作。卡小,那就把 KV cache 壓成 q8 省 VRAM,對吧?看看實際省了多少:q8 是 3412 MiB,f16 是 3427 MiB。約 15MB。對,就 15MB。

原因是 n_swa=512。用了 sliding-window attention,多數 layer 只留最後 512 token 的 KV,所以就算 context window 開到 16K,KV cache 本來就很小,幾乎沒東西可壓。這張卡上真正吃 VRAM 的是模型權重——Q4_0 檔約 3.2GB,差不多佔了常駐的 94%。去量化 KV cache,是在優化那個不是問題的 6%。

而且這是有代價的。用 q8 KV 反而把 decode 從 42.5 拖慢到 32.3 tok/s——少掉 24%——因為 Maxwell 沒有快速 int8 路徑,每一步 decode 都要把剛讀進來的 KV 解一次量化。等於用四分之一的 decode 速度,去換那 15MB。我把 production 改回 f16 KV 了。

收穫

最花時間的地方

最花時間的不是 benchmark,是那個假設。我差點沒測「FA 開」,因為「那是 tensor core 的東西,Maxwell 卡不會有用」是那種感覺很篤定、會讓你直接跳過的信念。實際測它只花幾分鐘;不測的話,我長 prompt 的 decode 就白白少 43%,還換來一個撐不穩的 16K context。這次最險的,其實是我差點就信了那條經驗法則。

下次也用得上的判斷

在動那些一般直覺會去調的參數之前,先做兩個檢查。第一:沒有 tensor core 的卡,把 Flash Attention 開起來、用長 prompt 量 decode——它可能接近翻倍,因為 decode 卡在 KV 的記憶體搬運,不是卡 tensor core 的 matmul。第二:要量化 KV cache 之前,先看模型是不是 sliding-window——如果是,KV 很可能本來就很小,那就先量一下 q8 跟 f16 差多少 VRAM(這裡是 15MB),再決定值不值得拿 decode 速度去換。小卡想省 VRAM,真正有效的通常是動權重,不是動 cache。

通用原則

那套常見的 KV cache 建議——「FA 是 tensor core 的東西、把 cache 量化省記憶體」——背後假設的是一台新的、卡頻寬的、非 SWA 的機器。任何一個前提反過來,建議就跟著反。在一顆沒有 tensor core 的 SWA 模型上,FA 是 decode 贏也 VRAM 贏,而 KV 量化大多只是拿 decode 速度,去換一塊 sliding window 本來就不會用到的記憶體。一定要在自己手上那張卡實測——那些經驗法則,是為別張卡寫的。

結論:實際該怎麼設

  • 沒有 tensor core 的卡(GTX 9xx/10xx 那輩):Flash Attention 開下去-fa)。這裡它讓長 context 的 decode 接近翻倍,還省了約 430MB。
  • 一張 4GB 卡跑 16K context,FA 不是可選的——不開它,那一跑只剩 175MB free。
  • sliding-window 模型(Gemma 4 的 n_swa=512):KV 維持 f16。q8 只省約 15MB,賠掉 24% 的 decode。
  • 只有在模型不是 sliding-window、而且 context 長到 KV 真的佔 VRAM 大頭時,才去量化 KV cache。不然就去量權重——記憶體真正在那。

寫到第三篇,這張 GTX 970 已經跑過四種量化、一個完整的多模態語音助手,現在又跑了一條 16K context 的長 prompt——而每一次的教訓都一樣:教科書的答案,是為另一張卡寫的。唯一能信的數字,是你在面前這台機器上量到的那個。

最後一個我自己覺得很浪漫的點:讓 q8 KV 變沒意義的同一個 SWA 機制——KV 鎖在 512、context 幾乎免費——也代表這張卡撐 64K context 幾乎不吃記憶體(我後來真的把它開到 64K 拿去做別的活,見 Part 4)。64K 夠大,大到你甚至可以認真起念:在這張 2014 年的卡上掛一個 Hermes agent。一張十一年前的卡,跑著 2026 年的 agent——光想就有點浪漫。(不過基本上不建議啦:E2B 太小、SWA 的窗也擺在那,當個輕量小幫手可以,當真正的 agent 腦會卡。)


同系列其他篇:

常見問題

GTX 970 這種老卡該不該開 Flash Attention?
該開。在 GTX 970 上跑 Gemma 4 E2B、餵一條 7719 token 的 prompt,開 Flash Attention 讓長 context 的 decode 從 24.3 衝到 42.5 tok/s,接近翻倍(KV 都固定 f16)。它還順便省了約 430MB VRAM。「老卡開 FA 沒用、甚至會傷」這個一般印象,在這張卡上不成立。
把 KV cache 量化成 q8 真的能省 VRAM 嗎?
在這顆模型上不行。Gemma 4 E2B 用 sliding-window attention,多數 layer 的 KV 鎖在 512 token,cache 本來就很小。q8 KV 相對 f16 只省了約 15MB,還因為 Maxwell 沒有快速 int8,把 decode 從 42.5 拖到 32.3 tok/s。真正吃 VRAM 的是模型權重,不是 KV cache。
什麼時候才值得量化 KV cache?
只有在模型不是 sliding-window、而且 context 長到 KV 真的佔掉 VRAM 大頭時才值得。SWA 模型、或者小卡,真正能省 VRAM 的是模型權重,不是 KV cache——要量就量權重。