Files
2026-07-13 12:34:46 +08:00

107 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Full kernel benchmark: Cider per-channel & per-group vs MLX w8a16 & w4a16.
Tests M = {1, 128, 1024, 4096, 8192} across Qwen3-2B/7B shapes.
"""
import sys, os, time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np
import mlx.core as mx
from cider import perchannel_linear, pergroup_linear, quantize_weight_int8
WARMUP = 5
REPEAT = 20
# Qwen3-VL shapes: [N, K]
SHAPES_NK = [
(3584, 3584), # qkv square
(18944, 3584), # up-proj
(3584, 18944), # down-proj
(2560, 2560), # 2B square
(10240, 2560), # 2B up
(2560, 10240), # 2B down
]
M_VALUES = [1, 128, 1024, 4096, 8192]
GROUP_SIZE = 128
def timed(fn, warmup=WARMUP, repeat=REPEAT):
for _ in range(warmup):
y = fn(); mx.eval(y)
times = []
for _ in range(repeat):
t0 = time.perf_counter()
y = fn(); mx.eval(y)
times.append(time.perf_counter() - t0)
return np.median(times) * 1000
def main():
print(f"Full Kernel Benchmark (warmup={WARMUP}, repeat={REPEAT}, gs={GROUP_SIZE})")
print()
for N, K in SHAPES_NK:
print(f"=== Shape [N={N}, K={K}] ===")
print(f"{'M':>6s} | {'PC(ms)':>8s} {'PG(ms)':>8s} {'w8a16':>8s} {'w4a16':>8s} | {'PC/w8':>7s} {'PC/w4':>7s} {'PG/w8':>7s} {'PG/w4':>7s}")
print("-" * 95)
# Prepare per-channel weights
np.random.seed(42)
w_fp = np.random.randn(N, K).astype(np.float32)
# Per-channel: scale per row
absmax = np.abs(w_fp).max(axis=1, keepdims=True).clip(min=1e-8)
scale_pc = (absmax / 127.0).astype(np.float32)
w_int8_pc = np.clip(np.round(w_fp / scale_pc), -127, 127).astype(np.int8)
w_int8_pc_mx = mx.array(w_int8_pc)
scale_pc_mx = mx.array(scale_pc.squeeze())
# Per-group weights (symmetric, gs=GROUP_SIZE)
ng = (K + GROUP_SIZE - 1) // GROUP_SIZE
w_pg = w_fp.copy()
scale_pg = np.zeros((N, ng), dtype=np.float32)
w_int8_pg = np.zeros((N, K), dtype=np.int8)
for g in range(ng):
k0 = g * GROUP_SIZE
k1 = min(k0 + GROUP_SIZE, K)
blk = w_pg[:, k0:k1]
amax = np.abs(blk).max(axis=1, keepdims=True).clip(min=1e-8)
s = (amax / 127.0).astype(np.float32)
scale_pg[:, g] = s.squeeze()
w_int8_pg[:, k0:k1] = np.clip(np.round(blk / s), -127, 127).astype(np.int8)
w_int8_pg_mx = mx.array(w_int8_pg)
scale_pg_mx = mx.array(scale_pg)
# MLX w8a16 (bits=8, gs=64)
w_mx = mx.array(w_fp.astype(np.float16))
w8_q, w8_s, w8_b = mx.quantize(w_mx, bits=8, group_size=64)
# MLX w4a16 (bits=4, gs=64)
w4_q, w4_s, w4_b = mx.quantize(w_mx, bits=4, group_size=64)
mx.eval(w_int8_pc_mx, scale_pc_mx, w_int8_pg_mx, scale_pg_mx,
w8_q, w8_s, w8_b, w4_q, w4_s, w4_b)
for M in M_VALUES:
x = mx.random.normal((M, K)).astype(mx.float16)
mx.eval(x)
t_pc = timed(lambda: perchannel_linear(x, w_int8_pc_mx, scale_pc_mx))
t_pg = timed(lambda: pergroup_linear(x, w_int8_pg_mx, scale_pg_mx, GROUP_SIZE))
t_w8 = timed(lambda: mx.quantized_matmul(x, w8_q, w8_s, w8_b, bits=8, group_size=64))
t_w4 = timed(lambda: mx.quantized_matmul(x, w4_q, w4_s, w4_b, bits=4, group_size=64))
r_pc_w8 = t_w8 / t_pc if t_pc > 0 else 0
r_pc_w4 = t_w4 / t_pc if t_pc > 0 else 0
r_pg_w8 = t_w8 / t_pg if t_pg > 0 else 0
r_pg_w4 = t_w4 / t_pg if t_pg > 0 else 0
print(f"{M:>6d} | {t_pc:>6.2f}ms {t_pg:>6.2f}ms {t_w8:>6.2f}ms {t_w4:>6.2f}ms | {r_pc_w8:>5.2f}x {r_pc_w4:>5.2f}x {r_pg_w8:>5.2f}x {r_pg_w4:>5.2f}x")
print()
if __name__ == "__main__":
main()