
源碼精讀篇本文為源碼/方法論精讀無獨立實測文中數字均引述倉庫 docs 的板端實測記錄一句話導讀flash_attn_single_q_q8_neon單 query 對全量 KV 恒定單遍的 NEON 注意力內核逐段導讀 Q 量化一次、逐 token 打分與在線 rescale、尾部歸一講清單遍比兩遍省在哪三處。9-1 把 KV 壓成了 q8可 decode 要“每個 token 掃一遍全部歷史 KV”——怎么掃才不浪費答案是一個“恒定單遍”的 NEON 內核flash_attn_single_q_q8_neon。它把 8-2 的在線 softmax 與 q8 KV 合體量化 Q 一次、掃 KV 一遍、邊掃邊 rescale、讀完即出結果。1. 知識點decode 的注意力為什么能“單遍”decode 時 query 只有一個當前生成詞要對它和全部歷史 KV 算注意力。教科書寫法8-1要三遍先算完全部分數、再 softmax、再加權 V——這意味著把全部分數存下來或掃兩遍 KV。單遍版的數學基礎就是 8-2 的在線 softmax維護M已見分數最大值與S已見 exp 和每來一個 token 的分數當場決定分數 ≤ Mw exp(score - M)直接累進 V 加權和分數 M先把已累加的和整體乘exp(M_old - M_new)rescaling再繼續。于是 KV 只掃一遍、分數不必物化、輸出是“邊掃邊累積”的——這就是“恒定單遍”無論上下文多長KV 只被讀一次。2. 對應代碼逐段導讀第 6594–6670 行函數簽名先交代了 q8 KV 的“三件套”第 6594–6602 行static void flash_attn_single_q_q8_neon( float *restrict attn_out, /* [hd] 歸一化輸出 */ const float *restrict q, /* [hd] query 激活fp32 */ int8_t *const *k_cache_q8, /* 每層 INT8 K 塊數組 */ int8_t *const *v_cache_q8, const float *restrict kscale, /* k_scale[l] khper-token per-head */ const float *restrict vscale, int seq_len, int kv_dim, int kv_bs, int nkv, int kh_off, int hd, float scale)注意kscale注釋“per-token per-head”——正是 9-1 那個每 (token, 頭) 的 scale。段 1Q 量化一次第 6609–6625 行/* 1. Q → INT8每 32 元素 1 max-abs scaleQ8_0 同構 */ for (int b 0; b nblk; b) { float qm 0.0f; for (int i 0; i 32; i) qm fmaxf(qm, fabsf(qb[i])); if (qm 1e-6f) qm 1.0f; qsc[b] qm * KVQ_INV; /* KVQ_INV 1/127 */ float iq 127.0f / qm; for (int i 0; i 32; i) qi[b*32i] (int8_t)clamp(qb[i] * iq, -128, 127); }q8 × q8 的點積要求兩邊都是 int8——K 已經是 int8所以把 Q 也量一次兩者就能直接走 vdot6-1 的i8x16_dot_s32。Q 只有 128 維量化成本微不足道且全函數只量這一次。段 2在線 softmax 運行態第 6627–6666 行8-2 已拆float M -1e9f, S 0.0f; /* 單遍 KV 掃描逐 tokenonline softmax 分塊語義 */ for (int t 0; t seq_len; t) { const int8_t *kt k_cache_q8[t / kv_bs] (size_t)(t % kv_bs) * kv_dim kh_off; float ks kscale[(size_t)t * nkv] * KVQ_INV; /* Q·K^T每 32 塊一個 int32 累加塊 scale 分別乘后求和 */ float s 0.0f; for (int b 0; b nblk; b) { ...acc i8x16_dot_s32(...); s dot32 * qsc[b]; } s s * ks * scale; /* online softmax新 max → rescale 已累積 VKQ */ if (s M) { S * expf(M - s); M s; /* acc 同步縮放 */ ... } else { vsf expf(s - M); } S vsf; /* VKQ INT8 V × (vsf × vs)softmax 權重保持 F32 */ ... }三個要點塊 scale 的歸位K 的 int8 值與 Q 的 int8 值點積得到“無 scale”的 int32 部分和真正的浮點值要乘qsc[b]Q 每 32 塊 scale×ksK 每 token 頭 scale——整數點積只算骨架浮點標定在累加時補齊Day 6–7 的 dotprod 紀律在 KV 上的再現kv_bs分塊t / kv_bs定位塊、t % kv_bs定位塊內 token8-3 的布局在線 rescale遇新 max 只縮放已累計量KV 掃完即得未歸一結果。段 3歸一輸出函數尾部attn_out[i] / S連同最終 scale 處理一次除法收尾。3. 改動后果把在線 rescale 去掉改回“先存全部分數”假設改成樸素兩遍先把 seq_len 個分數都算好存進數組float scores[8192]之類再找 max、再統一 exp——功能等價代價三樣多一遍 KV 讀取打分一遍、加權 V 又一遍 → KV 掃描 ×2q8 KV 的帶寬紅利白省一半多一份分數數組8K 上下文要 32KB 棧/堆暫存還破壞 cache 局部性失去“流式”能力在線版可以在 KV 逐塊到達跨進程磁盤 KVDay 12時邊讀邊算兩遍版必須整段就緒。這正是技術文檔里那句“q8-KV decode 恒定單遍”的含義技術文檔.md 第 293 行——單遍不是風格是帶寬與流式能力的共同要求。誠實標注文檔還注明“x86 實驗開關VLLM_ATTN_ONLINE未進入本 aarch64 樹”——即 ARM 發布樹固定走本內核的在線路徑沒有可切換的“非在線”后端。本節“去掉 rescale”是概念對照板端無對應開關。4. 學員調試任務A 檔板端動手在vllm_safetensors.c第 6594 行函數內打斷點用 1-3 的 Debug 構建姿勢跑一次生成觀察進入函數時seq_len是多少、單次調用內for (t)是否恰好掃seq_len遍、M是否單調不降。B 檔純讀源碼逐行標出“Q 量化 / 打分 / rescale / 歸一”四段的起止行號回答為什么 Q 只量化一次而 KV 的 scale 每 token 都要乘預期輸出你能畫出“單 q × 全 KV 單遍”的數據流Q→int8、逐 token 打分在線 softmax、尾部歸一并說清它比兩遍版省在哪三處。收尾本篇源碼點名vllm_safetensors.cflash_attn_single_q_q8_neon第 6594 行起、技術文檔.md 第 293 行。開源倉庫Kestrel-LLM (Gitee)源碼可得雙許可學習 / 學術研究免費下篇預告單遍掃全 KV 很優雅——可它“算得準嗎”下一篇 9-3 做 q8 KV 與 fp32 KV 的精度對照看量化在注意力輸出上到底差多少。關鍵詞q8 KV、flash attention、NEON、單遍掃描、在線 softmax上一篇Day 9·1 KV 也量化——q8 KV 把緩存與 decode 帶寬壓到一半下一篇Day 9·3 q8 KV 精度對照——量化進注意力輸出差多少