跳轉到

Benchmarks

這一頁把 Story 的 pack-once 設計放回完整 measurements,界定 它在哪些 workload 勝出、持平或仍然落後。

Result Map

PR #1256 量測一個 targeted Cartesian family:

Text Only
2 dtypes
x 3 broadcast positions: LHS, RHS, cross
x 2 BLAS-incompatible inner layouts: reverse, step 2
x 6 affected sizes: 16, 24, 32, 64, 128, 256
= 72 cases

這 72 點都會走 PR 新增的 pack-once route。下圖逐格顯示公開 JSONL 重算出的 NumPy median / solvcon median1.00x 是同速,大於 1.00x 才是 solvcon 較快。

PR 1256 全部 72 個 targeted Cartesian cases 相對 NumPy 的 speedup heatmap
72 點全部高於 `1.00x`;最弱 `1.11x`、中位 `1.64x`、最強 `2.97x`。 橘框標出最弱點。這是單一機器上的 targeted family,不是跨平台排行榜。

圖裡也能看出結果不是「矩陣越大,優勢越大」。float32 的高點集中在 S=64, float64 多數在 S=32;到了 S=256,float64 的優勢只剩 1.15x–1.22x。 最低的 1.11x 出現在 float64、cross broadcast、step-two、S=16。該點的 90 筆 estimates 中,NumPy median 是 0.008119 ms,IQR 為 0.008051–0.008239 ms;solvcon median 是 0.007331 ms,IQR 為 0.007116–0.007600 ms。IQR 描述 raw samples 的分布,不是 confidence interval。最靠近 crossover 與最大尺寸的 margin 並不寬。

同一次實驗另有 S=8S=12 的 24 個 controls。它們沒有改變 route,不在 上圖的 72 格中;24 點仍全部由 NumPy 領先,solvcon runtime 是 NumPy 的 1.37x–2.33x。這正好保留了 policy boundary 兩側的結果。

Other Routes

其他 PR 補上「持平」與「仍然輸」的區域:

  • #1256 reused incompatible matrix, pack once:72/72 勝出,範圍 1.11x–2.97x。這就是本頁 heatmap 的 targeted family。
  • #1209 C-contiguous S=256 batched GEMM:ratio 為 0.951–1.031。 兩邊使用同一 Accelerate backend,大致持平。
  • #1259 reused negative-stride vector, pack once:32/32 勝出,median 6.24x。vector reuse 也能攤提 packing。
  • #1259 positive-stride vector, direct BLAS:16/80 勝出,median runtime 是 1.06x NumPy。direct BLAS 消除舊 generic 成本,但不保證贏 NumPy。

這張表的分界比「用了 BLAS」更具解釋力。兩邊直接進入同一個 backend 時,首先 應期待 parity;能跨 contractions 少付重複 layout 成本時,才出現穩定勝出的 targeted family。

Measurement

1256 的正式資料來自 native macOS 26.5.1 arm64、Python 3.14.6、NumPy 2.5.1

與單執行緒 Apple Accelerate。每個 workload 比較 NumPy、PR parent、PR head, 並量完整的 np.matmul()matmul_planned() call,不只抽出 BLAS kernel。

每個 method 先執行一次 discarded preconditioning。接著六個 rounds 走遍三種 methods 的六種 execution orders。每個 method 在每輪先跑兩個 untimed warm-up batches,再收集 15 個 calibrated timed batches;每個 timed batch 約 30 ms, elapsed time 除以 inner-loop count 得到 per-call estimate。每個 method 因此有 6 x 15 = 90 筆 estimates,圖中每格使用 untrimmed median。

完整 artifact 有 96 workloads x 3 methods x 6 rounds = 1,728 筆 JSONL records。inputs、conversion、warm-up 與 correctness checks 不計時;dispatch、 output allocation、kernel execution 與 return 都在 timer 內。96 個 workloads 全部通過 numerical check,最大 relative Frobenius error 是 1.32e-16

Calibration

Result map 是 merged selector 在 targeted family 上的 measurements;threshold calibration 是另一件事。 PR #1209 以相同 inputs 強制執行 generic 與 CBLAS,量出不同 role 的 crossover。這些 measurements 形成當時 merged policy,例如 DOT K >= 128 與 GEMM min(M,N,K) >= 8

完整 boundaries 與 selector 條件放在 Dispatch。 它們是該機器與 backend 的實測 policy,不是 Matmul semantics,也不是 portable performance constants。

Audit

公開 artifact 保留了重算上圖所需的資料。下載 raw data 後,以下腳本會 檢查 record 數量、每格的 90 筆 samples、72 格的 Cartesian coverage,並輸出 heatmap 使用的全部 ratios 與 summary。

下載 #1256 的 1,728-record JSONL

顯示 artifact audit 指令
Bash
curl -L \
  https://github.com/user-attachments/files/30812808/pr1256-exact-head-profiling.jsonl.txt \
  -o pr1256.jsonl

python3 - pr1256.jsonl <<'PY'
import collections
import json
import statistics
import sys

samples = collections.defaultdict(list)
records = 0
with open(sys.argv[1], encoding="utf-8") as stream:
    for line in stream:
        row = json.loads(line)
        key = (row["dtype"], row["case"], row["size"], row["method"])
        samples[key].extend(row["samples_ms"])
        records += 1

assert records == 1728
assert all(len(values) == 90 for values in samples.values())

medians = {
    key: statistics.median(values)
    for key, values in samples.items()
}
dtypes = ("float32", "float64")
cases = (
    "cross_broadcast_negative",
    "cross_broadcast_step2",
    "lhs_broadcast_negative",
    "lhs_broadcast_step2",
    "rhs_broadcast_negative",
    "rhs_broadcast_step2",
)
sizes = (16, 24, 32, 64, 128, 256)

ratios = {}
for dtype in dtypes:
    for case in cases:
        row = []
        for size in sizes:
            key = (dtype, case, size)
            ratio = (
                medians[key + ("numpy",)] /
                medians[key + ("after",)]
            )
            ratios[key] = ratio
            row.append(ratio)
        print(dtype, case, *(f"{value:.2f}" for value in row))

assert len(ratios) == 72
values = list(ratios.values())
print("all above 1:", all(value > 1 for value in values))
print("minimum:", f"{min(values):.2f}x")
print("median:", f"{statistics.median(values):.2f}x")
print("maximum:", f"{max(values):.2f}x")
PY

預期 summary 是:

Text Only
all above 1: True
minimum: 1.11x
median: 1.64x
maximum: 2.97x

artifact 也記錄 benchmarked revisions:parent d3464749、PR head d4c1c98e、 NumPy 2.5.1。這是重算既有 measurements,不是重新執行 benchmark。

Re-run

1256 公開了完整 raw JSONL,但產生這 1,728 records 的 exact harness 沒有納入

版本控制。目前能精確重算 artifact,尚不能用 repository checkout clean re-run 同一組 cases、process ordering 與 timing protocol。完整 rerun 需要補上當時的 harness source、環境建置與 thread-control command。

Sources

#1209 與 #1259 的 measurement protocols

1209 使用五個 rounds,每 round 兩次 warm-up 與 15 次 individually timed

calls,再取 75 筆的 untrimmed median。

1259 使用 persistent processes、約 15 ms 的 calibrated timed batches

與 12 個 rounds;六種 method orders 各出現兩次。不同 PR 的 samples 沒有 混成跨實驗 aggregate。