Day01 到 Day09 走了兩條 RISC-V CPU 最佳化路線。第一條從 RVV intrinsic 開始,手寫 C++ kernel,再透過 torch.ops.sgl_kernel.* 接進 SGLang。第二條沿著 torch.compile 與 TorchInductor,讓一般 PyTorch Linear 產生 RVV BF16 GEMV / GEMM,今天讓我們來複習前面9天學到的知識。

這張圖要從左上角開始,沿著箭頭往下讀。第一段是硬體與向量基礎:Day01 把 K1 規格翻成軟體限制,Day02 學會 RVV intrinsic,Day03 再把 Python runtime、custom op 和 C++ kernel 接起來。
第二段進入手寫 SGLang RVV kernel。Day04 處理 Linear / GEMM,Day05 處理 Norm、activation 和 RoPE,Day06 則進入帶有 KV cache 與 online softmax 的 attention。這一段的共同點是我們直接控制資料配置、向量迴圈和 kernel 介面。
第三段換成 compiler 路線。Day07 先看懂 torch.compile 的 CPU pipeline,Day08 把 RVV 接進 Inductor codegen,Day09 再處理 packed weight、SGLang 整合和重現環境。

我們 Day01 介紹 SpacemiT K1 的硬體規格
左上角的 ISA profile 會決定 -march、ABI 和工具鏈;右上角的 RVV 1.0 / VLEN 256 會影響 vsetvl、SEW、LMUL 與 VLMAX。左側的 8 cores 提醒我們分開考慮單一執行緒內的 RVV 向量化,以及多核心的外層 thread parallelism。右側的 L2 cache / LPDDR4X 則連到 tiling、資料重用,以及程式最後是算力受限還是記憶體頻寬受限。
下方兩條線是 benchmark 容易忽略的條件。標準 RVV 和廠商 AI 指令使用不同 API 與工具鏈,效能結果要分開歸因;DVFS / TDP 會讓頻率和溫度隨時間改變,因此需要固定執行緒、重複測量並報告中位數。Day01 建立的習慣是:讀到一條規格,就繼續問它會影響哪一段程式與哪一個測量條件。

Day02 分享了很多 RVV Intrinsic 相關知識,介紹要怎麼樣讀 RVV Intrinsic
圖的上半部是一輪向量加法。程式先計算 remaining = n - i,把它交給 vsetvl_e32m1。硬體回傳本輪的 vl 後,vle32 從 A、B 各載入 vl 個 FP32,vfadd.vv 同時相加,再用 vse32 寫回輸出。最後依照實際回傳的 vl 更新 i 和 remaining,回到迴圈起點。
圖的下半部把 VLEN、SEW 和 LMUL 放進同一個例子。K1 的 VLEN 是 256 bits;當 SEW 是 32、LMUL 是 1 時,一組向量暫存器最多容納 8 個 FP32,也就是圖中的八格。這個 8 是 VLMAX,不代表每一輪的 vl 都必須等於 8。
最下面的 n=20 說明 tail handling。硬體可能回傳 8、8、4,也可能採用其他合法分段;程式只依照每輪回傳的 vl 前進。這套 strip-mining 迴圈自然處理最後不足一個向量的元素,後面的 GEMM、Norm 和 attention kernel 都沿用同一個觀念。

Day03 介紹 SGLang 是什麼,介紹 Radixattention,然後說明 SGLang attention backend 流程。
圖的頂端是兩個請求共用的 system prompt。RadixTree 記錄 token 前綴與 KV cache 位置的對應,因此請求 A 和請求 B 都可以命中同一段最長前綴。中間虛線框標出的 shared region 只保存一份 system prompt KV,兩個請求各自的新問題才需要做新的 prefill。
這張圖也劃出 runtime 和 attention backend 的責任邊界。RadixTree 回答「哪些 token 已經算過、KV 放在哪裡」,attention backend 回答「拿到 Q、K、V 與索引後要怎麼算」。後面寫 RVV attention kernel 時,kernel 不會自己建立 RadixTree,它消費 runtime 整理好的 KV cache 位置。

左半邊是 decode。輸入包含當前 token 的 Q、既有 K/V cache,以及 sequence length 和 cache 位置等 request metadata。Python 先整理 shape、輸出與 logits buffer,再呼叫 decode_attention_cpu。C++ RVV kernel 沿著每個請求的歷史 token 讀取 KV,輸出下一層模型需要的 attention result。
右半邊是 extend / prefill。這裡一次處理一段新 token,輸入同時包含 prefix KV cache、新 token block,以及 len、start_loc 等 extend metadata。Python 把前綴與本輪 extend 的資料關係整理好,再交給 extend_attention_cpu 做 tiled attention;輸出除了送往下一層,也會擴充 KV cache。
兩邊可以共用同一個 SGLang backend 介面,資料流和 kernel 仍然不同。Decode 的 query 很短、主要掃過舊 KV;extend 有多個 query token、causal mask 和新的 KV 寫入。這也是為什麼 Day06 我們需要分別設計兩種 attention 分塊的原因。

Day04 開始分不同 AI 運算子的介紹
Hidden states 先進入 RMSNorm,再通過 Q/K/V projection、RoPE 和 attention,最後與上方虛線表示的 residual path 相加。接著第二次 RMSNorm,進入 MLP 的 gate / up projection、activation、逐元素乘法和 down projection,再與另一條 residual path 相加,形成下一層的輸入。
Day04 關注 Q/K/V、gate/up、down 都是 Linear,核心問題是 GEMV / GEMM shape、weight packing、BF16 / FP16 載入與 FP32 累加。
Day05 關注另外三種計算。RMSNorm 需要做 reduction;RoPE 必須按照 head 與 rotary 維度配對資料;activation 需要處理近似函式和逐元素運算。
每個運算子有不同的資料重用方式、正確性條件與向量化策略,residual path 也要求輸入輸出的 shape 和 layout 保持一致。

Day06 會提到重要的 Flash Attention 演算法,和 FlashDecoding 觀念
左半邊是 FlashDecoding-style dataflow。一個當前 token 的 Q 會同時掃過多段 KV sequence。每個 KV split 各自計算 QK、online softmax 和 PV,保留 partial output 與 log-sum-exp(LSE)。最後再依照各段 LSE 的權重合併 partial outputs,得到完整 decode attention result。CPU thread 可以在不同 KV splits 之間平行。
右半邊是 FlashAttention-style dataflow。一個包含多列 query 的 Q tile 會逐塊讀取 prefix 和 current K/V tiles,計算小型 score tile,更新 online softmax 的 m_acc、l_acc,並重新縮放已累積的 v_prime。所有 K/V tiles 掃完後才 normalize 並寫回輸出;current tile 還需要套用 causal mask。
兩條路徑都只保留小型 score tile 和 running statistics,避免配置完整 attention score matrix。RVV 負責 tile 內的 load / gather、FMA、FP32 reduction 與 exp 近似,CPU threads 則負責 tile 或 KV split 之間的平行

Day07 介紹了 PyTorch2.0 論文,仔細說明 torch.compile 整個流程。
圖的上半部是 CPU 與 GPU 共用的 compiler frontend。Python function / nn.Module 交給 TorchDynamo 後,Dynamo 擷取 tensor operation 並建立 guards,輸出 FX Graph。
AOTAutograd 接著做 decomposition 與 functionalization,運算整理成 ATen / Prim 形式,再交給 TorchInductor lowering 與 fusion。
到 target backend 才分成 CPU 和 GPU。圖右保留 GPU 對照線,我們主要探討 CPU 的部分。Inductor 的 CPU backend 會建立 loop-level IR 與 schedule,產生 C++ / OpenMP source,交給 GCC / Clang 編譯與連結成 .so,最後由 Python 載入並呼叫 compiled kernel。
https://ithelp.ithome.com.tw/upload/images/20260809/20183319UwcJpIwr4w.png
Day 08 開始講我的 Poster 實作內容,從最上方往下看,PyTorch model / function 經過 torch.compile(),TorchDynamo 擷取 Python bytecode 並建立 FX Graph。AOTAutograd 整理 graph 後,預設 backend TorchInductor 接手,接著進入 CPU codegen。
CPU codegen 下方原本已經有 x86 / Intel 與 ARM 路徑。紅框標出的 RISC-V RVV 是這次補上的 backend / codegen 方向:P1 讓 Inductor 能選到 VecRVV 並帶入 macros、-march;P2 為 BF16 Linear 提供 M=1 GEMV 與 M>=2 GEMM microkernel;P3 只把支援的靜態 BF16 shape route 進 RVV template,其餘情況保留 fallback。三條 CPU target 路徑最後都要產生 compiled CPU kernels。
紅框也界定了目前貢獻的範圍。這次實作沿用 Inductor 已有的 graph capture、lowering 與 C++ template 架構,在 microkernel template 中產生 riscv_vector.h intrinsic,再由 GCC 編成 RVV 指令。目前沒有新增一套 RVV IR,也沒有加入跨多層 IR 的 rewrite、跨算子融合或全圖 scheduling pass。
P4 把 BF16 weight 從 [N, K] 轉成 [ceil(N/32), K, 32]。Packed tensor 由 SGLang module 持有,而且設為 non-persistent buffer。原始 row-major weight 仍然保留,packed state 過期或記憶體不足時可以 fallback。
P5 再把這些 compiler 能力接進 SGLang 原本的 TorchNative model-load 與 serving flow。P5 commit 讓使用者保留原本的 --device cpu --dtype bfloat16 --attention-backend torch_native,只新增 --enable-cpu-rvv-inductor。
這個高階 flag 會選擇已測試的 regional compile、explicit packed BF16 weights 與 small-batch buckets。Policy 在正常 checkpoint load 後安裝,負責 model adapter、projection discovery、packed side-state ownership、memory budget、tied lm_head、compiled shape key 與 fallback。
P6 最後固定 source branch、container image、model revision 和 cache state。這些條件決定另一台 Banana Pi 能否重建相同的 generated C++、.so 和 benchmark。Compiler 能產生 kernel 只是中間結果,runtime ownership、fallback、artifact 數量和實驗環境都會影響這條路能不能進入服務。
硬體規格和軟體之間的關聯是什麼 ISA、VLEN、核心數、cache、LPDDR 和 DVFS 分別影響 compiler flags、向量迴圈、執行緒、資料重用與 benchmark。規格表和程式、測量方法之間的工程意義。
RVV kernel vl 的處理。 VLEN、SEW 和 LMUL 決定 VLMAX,因為 RISC-V 是 VLA(Vector-Length Agnostic),會利用 vsetvl 決定當下這一輪實際處理多少元素。Strip-mining 同時處理完整向量與尾端資料,避免把 K1 的固定寬度寫死在演算法裡。
Runtime metadata 是 kernel 輸入的一部分。 Attention 除了需要 Q、K、V,還需要 sequence length、KV cache 位置、prefix 命中與 causal 範圍。Python runtime 先把這些關係整理清楚,C++ RVV kernel 才能沿著正確位置計算。
不同運算子需要不同的向量化與平行化策略。 Linear 重視 weight layout 與累加,Norm 重視 reduction;RoPE 重視成對資料配置;decode / extend attention 則需要不同的分塊與 online softmax。共同使用 RVV intrinsic,不代表它們適合共用同一個 kernel 結構。
目前的 Inductor RVV 貢獻位在 target-specific C++ template codegen。 P1–P3 補上 ISA selection、compiler flags、BF16 microkernels 與靜態 shape routing,沿用原本的 Dynamo、FX、lowering 和 template 架構。這個範圍和新增 RVV IR、跨 IR rewrite 或全圖最佳化需要分開描述。
最佳化 layout 需要明確的 ownership、失效規則與 fallback。 Packed weight 可以提高 RVV Linear 的資料存取效率,也會增加記憶體並產生 runtime side state。Module 必須知道誰持有 packed tensor、weight reload 後何時失效,以及失敗時如何回到 row-major F.linear。
效能結論需要有路徑證據與實驗條件。 Generated RVV intrinsic、compiled .so、fallback、artifact-cold / warm cache、正確性、throughput、latency、RSS、source 與 model revision 都要一起記錄。單一加速比無法說明收益來自 kernel、cache、packing 還是 workload 變化。
謝謝大家看到這裡,已經完成三分之一啦,明天開始會換到 NVIDIA GPU,前面在 RISC-V CPU 學到的問題仍然會保留,硬體提供什麼執行模型、compiler 怎麼做 lowering、kernel 怎麼寫、runtime 怎麼分派、效能測試怎麼證明。下一次硬體會變成 CUDA、Triton 和 GPU 記憶體階層。