文件導航

快速後端

TileLang 可移植性

laya/tl_kernels.py 裡的五個核心都能通過 compile_cpu 在 TileLang 的 CPU(c)目標上執行。這是一種顯式的標量 fp32 特化,不是 Agent.accelerate 的 CPU 加速後端。它需要 TileLang 和一個本地 C++ 編譯器。它不引入任何執行時服務依賴。

import torch
from laya import tl_kernels as K

A = torch.randn(17, 67)
W = torch.randn(70, 67)
b = torch.randn(70)
C = torch.empty(17, 70)
kernel = K.compile_cpu(K.gemm_kernel, 70, 67, bias=True, act="gelu")
kernel(A, W, b, C)

compile_cpu(factory, *args, **kwargs) 接受與五個 GPU 工廠相同的形狀與操作選項。它選擇 cpu=True、dtype="float32"、目標 c,並關閉向量化。所有浮點輸入和輸出都必須是連續的 CPU float32 張量;注意力長度仍為 int32。呼叫它之前,用 .cpu().float().contiguous() 轉換 16 位啟用。LayerNorm 仍然原地更新它的殘差張量,RoPE 仍然原地更新 Q/K 列。保留編譯好的核心,以複用它的動態維度。

CPU 特化把 fragment 和 shared 分配替換為區域性緩衝區,使用序列的 T.grid 迴圈,並省略注意力的 GPU swizzle 標註。GEMM 使用 TileLang 的標量 CPU 實現,歸約使用區域性緩衝區。原本的分塊演算法、填充謂詞、注意力掩碼和線上 softmax 仍與 GPU 實現共用。CPU 中間結果保持 fp32,包括注意力機率;GPU 中間結果保留其原始 dtype。這裡不宣稱任何 CPU 加速或全模型 CPU 後端。

在 TileLang 0.1.14 上重新探測

觀測環境:Linux、Python 3.12.13、torch 2.11.0+cu130、TileLang 0.1.14 和一塊 RTX 4070 Ti SUPER。早先的可移植性顧慮針對的是編譯未經改動的 GPU 特化,而不是 CPU 降低的可能性。在基線提交 fa9a2a7 上,checkout 裡沒有這個文件頁。

測試用 target="c" 和 GPU 的 FAST pass 配置編譯 factory.get_tir(...)。以下是確切的診斷文本(省略了原始碼位置和堆疊蹤跡)。每個核心都探測了 bf16 和 fp16。

核心與探測維度 首個 bf16 失敗 首個 fp16 失敗
gemm_kernel(128, 64) Check failed: layout_map.count(buffer) != 0 (0 vs. 0) : The layout for fragment C_l can not be inferred correctly. 同上
gemm_geglu_kernel(64, 64) CPU fill only supports local and global buffers, but got dst scope `local.fragment`. 同上
add_ln_kernel(128) CPU reduce only supports local src and local/local.var dst buffers, got src scope `local.fragment` and dst scope `local.fragment`. 同上
rope_kernel(2, 64) Cannot convert type bfloat16 to C type C++ 編譯器:error: no matching function for call to ‘vec_type<float, 4>::vec_type(half4&)’
attn_kernel(1, 64, 2, 64) Check failed: layout_map.count(buffer) != 0 (0 vs. 0) : The layout for fragment s_c can not be inferred correctly. 同上

前四個 fragment/歸約失敗是 tvm.error.InternalError。bf16 程式碼生成失敗也是 InternalError。FP16 RoPE 丟擲 RuntimeError: Compilation Failed!,後面跟著編譯器呼叫和原始碼;上面的診斷文本輸出到 stderr。它生成的向量轉換在把 float 向量轉回 half 向量時也失敗了。

為了把 dtype 支援與 fragment 支援分開,測試用 cpu=True(區域性緩衝區和序列迴圈)再次編譯每一個核心,保留 bf16 並關閉向量化。此時五個核心都以完全相同的方式失敗:

Cannot convert type bfloat16 to C type

因此,fragment 是 GEMM、GEGLU 和注意力的第一道阻礙,fragment 歸約是 LayerNorm 的第一道阻礙,而 bf16 獨立地成為全部五個核心的阻礙。RoPE 沒有 fragment 分配或歸約;GEMM 和 GEGLU 沒有顯式的 T.reduce_* 操作。注意力的歸約起初被它的佈局失敗掩蓋了。僅針對 fp32 的單獨 fragment 探測,在沒有任何 GEMM 或 bf16 的情況下,為 reduce_sum 和 reduce_max 都復現了填充錯誤和歸約錯誤。只替換 scope 也產生了 GEMM 緩衝區的這個語義錯誤(其它受影響的緩衝區是 Ci、x 和 s):

[Tilelang Semantic Check] Local buffer `C_l` is indexed by T.Parallel loop variable `i`. Local buffers are thread-private and do not participate in parallel layout inference. Use T.serial/T.vectorized/T.unroll for per-thread local indexing, or T.alloc_fragment when the indexed dimension should be distributed across threads.

序列的 fp32 特化移除了這些阻礙。診斷斷言固定在 0.1.14 版本,在其它版本上跳過,那些版本上應當重新探測這些錯誤。數值 CPU 測試在其它版本上繼續執行。

數值校驗

執行 python -m pytest tests/test_fast_cpu.py -q -s。在上述環境上:58 通過。僅 CPU 的檢查不需要 CUDA;只有 GPU 對比在沒有 CUDA 時跳過。覆蓋範圍包括不均勻的 GEMM M/N/K 分塊、所有 GEMM 尾聲(epilogue)、GEGLU、所有殘差/偏置組合、大的殘差值、RoPE 位置迴繞與未被觸碰的 V 列、靜態/動態注意力形狀、滑動視窗、部分分塊、不相等長度、空序列,以及有限的填充輸出。

隨機種子是 1234。GPU 對比使用相同的輸入值,先舍入到 bf16 或 fp16,再升到 fp32 供 CPU 執行。這些是有界夾具的絕對容差,不構成對任意量級或模型深度的保證。注意力對比使用有效的 query 行,與 tests/test_fast.py 中一樣。

核心 CPU 與 fp32 參考的最大差 CPU 與 GPU 的最大差(兩種 dtype) CPU/GPU 容差
GEMM 2.38419e-7 0.00770831 0.05
GEGLU 2.98023e-8 0.000208303 0.05
LayerNorm 1.07288e-6 0.0156183 0.05
RoPE 0 0.0130053 0.05
Attention 5.96046e-7 0.00377572 0.02

CPU/參考容差是 2e-5(RoPE 為 2e-6)。殘差流更新完全相等,包括無殘差的情形。

GPU 保持的證據

cpu 關鍵字預設為 false。既有的 bf16/fp16 預設值、GPU 分配 scope、並行迴圈、swizzle 和 FAST 選項均未改變。預設的 fp32 GPU 呼叫仍然丟擲 ValueError。

前後對比執行分別使用了通過 git show fa9a2a7:laya/tl_kernels.py 取出的原始模組和改動後的模組。基線執行時,原始模組通過 importlib 以 laya.tl_kernels 載入;其它 Laya 程式碼和 Python 環境保持不變。

  • python -m pytest tests/test_fast.py -q,其中全前向測試被配置為載入下面標識的快取英文 checkpoint:改動前 13 通過;改動後 13 通過。最初沒有 checkpoint 時是 11 通過 / 2 跳過。兩次完整執行都輸出了該 checkpoint 既有的溫度鉗制警告(choice:11+=0.10058280825614929 -> 0.5)。
  • 對既有核心測試做帶種子的捕獲:24 個輸出張量逐位相同,包括殘差更新;前後最大差值為 0。
  • 上述五個探測形狀、兩種 dtype 生成的 CUDA 原始碼:10/10 逐位元組相同。這也覆蓋了 RoPE,它不在原來的 fast 測試套件裡。
  • python benchmarks/parity_fast.py --model "$MODEL" --dtype bf16 --json ... 以及等價的 fp16 命令:每個 dtype 288 個問題、60 個 state。所有前後 JSON 記錄(fp32、原裝和 fast 機率)比較完全相等;前後最大機率差值為 0。

MODEL 是快取的 convaiinnovations/laya 英文快照 55cf4c4ebb4ebe31b2550e8bdf3bd21b99753851;執行時使用 HF_HUB_OFFLINE=1。這裡不宣稱做過多語言或型別化決策 checkpoint 的對比。

dtype type n fast 與原裝的最大差,前 = 後 fast/原裝的 argmax 一致率,前 = 後
bf16 choice 48 0.0310 47/48
bf16 noul 180 0.0756 180/180
bf16 score 60 0.0152 60/60
fp16 choice 48 0.0069 48/48
fp16 noul 180 0.0092 180/180
fp16 score 60 0.0040 60/60

倉庫檢查

所有命令都使用 /home/ckl/projects/S/laya/.venv/bin/python;ruff 和 zensical 來自那個 virtualenv 的 bin 目錄。

命令 結果
ruff check laya/ --select=E9,F63,F7,F82,F401,F811 --line-length=120 All checks passed!
python -m compileall -q laya/ tests/ 退出碼 0,無輸出
python tests/test_router.py 703 通過,0 失敗
python tests/test_criteria.py 198 通過,0 失敗
python tests/test_hooks.py 240 通過,0 失敗
python tests/test_hooks_api.py 415 通過,0 失敗
python tests/test_packaging.py 131 通過,0 失敗
uv pip install --python /home/ckl/projects/S/laya/.venv/bin/python -r requirements-docs.txt 檢查了 3 個包(已安裝);該 virtualenv 沒有 pip
zensical build --strict --clean No issues found;沒有 griffe: 行

新測試套件在打包測試既有的豁免項裡登記為需要可選的 TileLang extra 和一個 C++ 編譯器。API 契約測試固定了這個新增的僅關鍵字 CPU 選擇器、未改變的 GPU dtype 預設值和 compile_cpu 簽名,且不把 TileLang 引入基礎 CI 環境。

AOTInductor

DecisionModel 可以匯出、編譯進一個 .pt2 包,並用 torch._inductor.aoti_load_package 載入。這個包同時返回決策 logits 和動作 logits。分詞、填充、溫度校準和答案格式化仍然由呼叫方負責;這不會新增 Agent 後端,也不改變它預設的執行路徑。

已關閉的 PR #472 記錄了早先的 dtype 阻礙。動作頭的池化 state 和置信度特徵以 fp32 計算,即使模型的權重顯式是 bf16。沒有 autocast 時,第一個動作線性層因而收到 fp32 輸入和 bf16 權重。現在前向把拼接後的輸入轉成 head 的權重 dtype,只在 autocast 之外。Softmax、熵和返回的決策 logits 保留它們的 fp32 計算。既有的 fp32 參數 eager 推理,包括 fp16/bf16 AMP,保持其數值;混合的權重/autocast dtype 也保留 autocast 原本的轉換。

把一份單獨的評估副本轉成 bf16 匯出,在 autocast 之外。把 token 軸宣告為 16 * Dim("tokens16", ...),以滿足注意力對齊防護。行數和標記數可以各自獨立地動態。不要假設在 autocast 下捕獲的匯出能在該上下文之外打包:這個配方要使用顯式的權重 dtype。

離線復現

基於斷言的檢查預設使用一個隨機初始化的微型 ModernBERT,不訪問網路。要測量真實模型就傳入一個本地 checkpoint 目錄。缺少 CUDA、AOTInductor API 或 C++ 編譯器時會產生顯式的 SKIP;在受支援的安裝上,編譯和一致性失敗會讓檢查失敗。

python scripts/check_aoti.py --output-dir /tmp/laya-aoti-smoke
HF_HUB_OFFLINE=1 TORCHINDUCTOR_CACHE_DIR=/tmp/laya-aoti/cache \
  python scripts/check_aoti.py --model /path/to/local/multilingual \
  --output-dir /tmp/laya-aoti

輸出目錄裡有 decision.pt2、results.json、inputs.pt 和 eager.pt。JSON 包含完整的 nvidia-smi 快照、匯出/打包/載入耗時、包位元組數、延遲,以及兩個頭的最大絕對 logit/機率增量。二進位制產物和編譯器快取被有意排除在倉庫之外。

在 RTX 4070 Ti SUPER 上實測

2026-10-03,Linux x86-64,Python 3.12.13,torch 2.11.0+cu130,transformers 5.17.0,NVIDIA 驅動 615.71.09,16,376 MiB 視訊記憶體。Checkpoint:convaiinnovations/laya,multilingual 子目錄,revision 為 1c5edc17a7acd8701df6fc341c0d179f1c62c982。基線是 main fa9a2a7。完整的機器快照和未舍入的測量在 aoti_multilingual_rtx4070.json 裡。

測量 前 後
匯出 5.00 s 3.89 s
打包 16.59 s 後失敗 60.02 s
包大小 沒有產物 645,729,621 位元組(615.82 MiB)
在已初始化的程序中載入 不可用 0.447 s
在空快取的嶄新程序中載入 不可用 4.867 s

復現的失敗發生在打包階段,匯出已成功:mat1 and mat2 must have the same dtype, but got Float and BFloat16。每次執行都使用一份單獨且初始為空的 Inductor 快取;AMP 編譯在打包之前執行,所以打包耗時不是一個全新直譯器冷啟動的測量。新程序檢查只加載包和已儲存的輸入(沒有 checkpoint),並遮蔽了 torch.compile 和打包入口點。它復現了兩個頭的答案。PyTorch 仍然把一個小的 CPU AVX 能力探測編譯進了最初為空的快取;沒有編譯任何模型圖或 CUDA 核心。因此,在這套執行時上,「載入時沒有任何編譯器活動」這種籠統說法是不準確的。

同一批已儲存的輸入包含跨三個批次的七行決策,帶有真實分詞後的賬單/退款文本、choice/score/noul 問題、填充和不等的有效標記數。每個延遲都是十次預熱後五組、每組 30 次前向的中位數,使用同步的牆鍾計時。torch.compile(dynamic=True) 使用預設模式。這些延遲數字不包含顯式的 CUDA 圖、分詞、匯出或編譯。

行 × token × 標記槽 AMP eager 前 → 後(ms) AMP compiled 前 → 後(ms) 顯式 bf16 eager(ms) 顯式 bf16 compiled(ms) AOTI bf16(ms)
2 × 128 × 3 15.1173 → 14.1200 6.7117 → 6.9076 13.9635 4.8835 2.2674
1 × 256 × 3 15.4687 → 14.4156 6.7274 → 5.4772 14.7994 4.7250 2.4154
4 × 160 × 5 14.8452 → 14.5384 7.3872 → 5.5934 14.7115 5.1393 3.6018

這裡的 AMP 指 fp32 參數配 bf16 autocast。顯式 bf16 指權重和殘差流都是 bf16、不帶 autocast;要跟包做最接近的執行對比就用那幾列。顯式 bf16 eager/compiled 和 AOTI 在這次修復之前都不可用。

對比,取全部七行的最大值 決策 logits 決策機率 動作 logits 動作機率
既有 eager 前後對比,fp32 與 fp16/bf16 AMP 0 0 0 0
AOTI 與顯式 bf16 eager 0.5625 0.00659859 0 0
AOTI 與既有 bf16 AMP eager 0.4375 0.00769910 8.0 0

在兩個 AOTI 對比裡,決策和動作的 argmax 都在 7/7 行上一致。機率是原始 softmax 輸出,未經校準。動作分佈在這些輸入上已經飽和,所以它的機率增量為零並不意味著底層 logits 與 AMP 相同。顯式 bf16 compiled 前向也與顯式 bf16 eager 不同(最大決策 logit 增量 0.5,機率增量 0.00659859)。降精度編譯不是逐位精確的。

GPU 與桌面應用和其它 Python 作業共用;CPU 編譯也是共用的,且沒有鎖定頻率。這些是觀測到的耗時,不是隔離環境下的加速宣告。包住這些執行的 nvidia-smi 快照:

執行 / 快照 GPU 利用率 已用視訊記憶體 功耗 溫度 / 狀態
前 / 開始 28% 2,817 MiB 12 W 33°C / P8
前 / 結束 10% 8,560 MiB 23 W 36°C / P3
後 / 開始 0% 2,634 MiB 12 W 33°C / P8
後 / 結束 73% 6,476 MiB 160 W 42°C / P2

仍存在的限制

  • 該產物特定於這個 checkpoint、精度、PyTorch/執行時棧和 GPU 目標;沒有測試到其它硬體或 PyTorch 版本的可移植性。
  • 這項檢查匯出 rows 1–8、tokens 32–512(16 的倍數)、marker slots 2–8。它覆蓋三種形狀(包括與匯出示例不同的形狀),而不是這些範圍內的每一個點。一個有效選項可能使用帶填充的 marker slots;真正的一槽張量走單獨的單選項分支,需要單獨匯出。任意 token 長度和生產環境的分桶/派發層不在這次改動範圍內。
  • 七行驗證的是打包這一回歸,而不是廣泛的 checkpoint 準確率或校準後的置信度一致性。把匯出副本轉換成 bf16,與在 autocast 下保留 fp32 權重是不同的;既有 eager 路徑本身保持不變。
  • 該指令碼需要一套支援 CUDA 的 PyTorch 構建和本地編譯器工具鏈才能建立產物。.pt2 內嵌模型和 CUDA 核心;不涉及任何託管服務。