mirror of
https://github.com/kvcache-ai/ktransformers.git
synced 2026-10-26 12:03:45 +08:00
215 lines
7.9 KiB
Python
215 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Benchmark: Optimizer step performance with fragmented vs fused MoE LoRA parameters.
|
||
|
||
Demonstrates the Base-LoRA layout conflict:
|
||
- Forward/backward produces per-expert LoRA gradients as many small tensors
|
||
- Optimizer prefers consolidated, contiguous, batch-processable state
|
||
- With E experts × 3 projections × 2 matrices × L layers = 6EL individual params
|
||
- KT fuses these into 6L contiguous [E, ...] buffer params
|
||
|
||
X-axis: number of experts
|
||
Y-axis: optimizer.step() time (ms)
|
||
|
||
Simulates real Qwen3-MoE dimensions:
|
||
H=7168, I=2048, r=8, variable layers and experts.
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import time
|
||
import torch
|
||
import torch.nn as nn
|
||
|
||
|
||
def create_fragmented_params(num_experts, num_layers, H, I, r, dtype=torch.bfloat16):
|
||
"""Create individual per-expert LoRA parameters (vanilla PEFT style).
|
||
Total: 6 * num_experts * num_layers parameters.
|
||
"""
|
||
params = []
|
||
for _layer in range(num_layers):
|
||
for _expert in range(num_experts):
|
||
params.append(nn.Parameter(torch.randn(r, H, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(I, r, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(r, H, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(I, r, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(r, I, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(H, r, dtype=dtype, device="cpu")))
|
||
return params
|
||
|
||
|
||
def create_fused_params(num_experts, num_layers, H, I, r, dtype=torch.bfloat16):
|
||
"""Create KT-style fused buffer parameters.
|
||
Total: 6 * num_layers parameters (independent of num_experts).
|
||
"""
|
||
params = []
|
||
for _layer in range(num_layers):
|
||
params.append(nn.Parameter(torch.randn(num_experts, r, H, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(num_experts, I, r, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(num_experts, r, H, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(num_experts, I, r, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(num_experts, r, I, dtype=dtype, device="cpu")))
|
||
params.append(nn.Parameter(torch.randn(num_experts, H, r, dtype=dtype, device="cpu")))
|
||
return params
|
||
|
||
|
||
def total_elements(params):
|
||
return sum(p.numel() for p in params)
|
||
|
||
|
||
def fill_grads(params):
|
||
"""Simulate backward pass: fill all grads with random data."""
|
||
for p in params:
|
||
if p.grad is None:
|
||
p.grad = torch.randn_like(p)
|
||
else:
|
||
p.grad.normal_()
|
||
|
||
|
||
def bench_optimizer_step(params, optimizer, warmup=2, iters=5):
|
||
"""Benchmark optimizer.step() time."""
|
||
for _ in range(warmup):
|
||
fill_grads(params)
|
||
optimizer.step()
|
||
|
||
times = []
|
||
for _ in range(iters):
|
||
fill_grads(params)
|
||
t0 = time.perf_counter()
|
||
optimizer.step()
|
||
t1 = time.perf_counter()
|
||
times.append((t1 - t0) * 1000)
|
||
return times
|
||
|
||
|
||
def bench_grad_clip(params, max_norm=1.0, warmup=2, iters=5):
|
||
"""Benchmark gradient clipping."""
|
||
for _ in range(warmup):
|
||
fill_grads(params)
|
||
torch.nn.utils.clip_grad_norm_(params, max_norm)
|
||
|
||
times = []
|
||
for _ in range(iters):
|
||
fill_grads(params)
|
||
t0 = time.perf_counter()
|
||
torch.nn.utils.clip_grad_norm_(params, max_norm)
|
||
t1 = time.perf_counter()
|
||
times.append((t1 - t0) * 1000)
|
||
return times
|
||
|
||
|
||
def run_one_config(E, L, H, I, r, foreach, warmup, iters):
|
||
"""Run fragmented vs fused for one expert count. Returns dict."""
|
||
row = {"experts": E, "layers": L, "foreach": foreach}
|
||
|
||
# --- Fragmented ---
|
||
params_frag = create_fragmented_params(E, L, H, I, r)
|
||
n_elem_frag = total_elements(params_frag)
|
||
opt_frag = torch.optim.AdamW(params_frag, lr=1e-4, foreach=foreach)
|
||
n_frag = len(params_frag)
|
||
|
||
step_frag = bench_optimizer_step(params_frag, opt_frag, warmup, iters)
|
||
clip_frag = bench_grad_clip(params_frag, warmup=warmup, iters=iters)
|
||
|
||
row["fragmented_n_params"] = n_frag
|
||
row["fragmented_n_elements"] = n_elem_frag
|
||
row["fragmented_step_ms"] = min(step_frag)
|
||
row["fragmented_step_median_ms"] = sorted(step_frag)[len(step_frag) // 2]
|
||
row["fragmented_clip_ms"] = min(clip_frag)
|
||
del params_frag, opt_frag
|
||
|
||
# --- Fused (KT-style) ---
|
||
params_fused = create_fused_params(E, L, H, I, r)
|
||
n_elem_fused = total_elements(params_fused)
|
||
opt_fused = torch.optim.AdamW(params_fused, lr=1e-4, foreach=foreach)
|
||
n_fused = len(params_fused)
|
||
|
||
step_fused = bench_optimizer_step(params_fused, opt_fused, warmup, iters)
|
||
clip_fused = bench_grad_clip(params_fused, warmup=warmup, iters=iters)
|
||
|
||
row["fused_n_params"] = n_fused
|
||
row["fused_n_elements"] = n_elem_fused
|
||
row["fused_step_ms"] = min(step_fused)
|
||
row["fused_step_median_ms"] = sorted(step_fused)[len(step_fused) // 2]
|
||
row["fused_clip_ms"] = min(clip_fused)
|
||
del params_fused, opt_fused
|
||
|
||
assert n_elem_frag == n_elem_fused, \
|
||
f"Element mismatch: frag={n_elem_frag} vs fused={n_elem_fused}"
|
||
|
||
return row
|
||
|
||
|
||
def run_benchmark():
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--layers", type=int, default=4)
|
||
parser.add_argument("--experts", type=str, default="1,2,4,8,16,32,64,128,256")
|
||
parser.add_argument("--hidden", type=int, default=7168)
|
||
parser.add_argument("--inter", type=int, default=2048)
|
||
parser.add_argument("--rank", type=int, default=8)
|
||
parser.add_argument("--threads", type=int, default=0, help="0 = use default")
|
||
parser.add_argument("--warmup", type=int, default=2)
|
||
parser.add_argument("--iters", type=int, default=5)
|
||
args = parser.parse_args()
|
||
|
||
if args.threads > 0:
|
||
torch.set_num_threads(args.threads)
|
||
|
||
expert_counts = [int(x) for x in args.experts.split(",")]
|
||
H, I, r, L = args.hidden, args.inter, args.rank, args.layers
|
||
threads = torch.get_num_threads()
|
||
|
||
print(f"Config: H={H}, I={I}, r={r}, layers={L}")
|
||
print(f"Expert counts: {expert_counts}")
|
||
print(f"Per-expert elements: {2*(r*H) + 2*(I*r) + (r*I) + (H*r):,}")
|
||
print(f"Torch threads: {threads}")
|
||
print(f"Torch version: {torch.__version__}")
|
||
print()
|
||
|
||
all_results = {}
|
||
|
||
for foreach in [True, False]:
|
||
label = "foreach" if foreach else "no-foreach"
|
||
print(f"=== AdamW foreach={foreach} ===")
|
||
print(f"{'experts':>7} | {'params':>7} {'fused':>6} | "
|
||
f"{'elements':>12} | "
|
||
f"{'frag step':>10} {'fused step':>10} {'speedup':>8} | "
|
||
f"{'frag clip':>10} {'fused clip':>10}")
|
||
print("-" * 105)
|
||
|
||
results = []
|
||
for E in expert_counts:
|
||
row = run_one_config(E, L, H, I, r, foreach, args.warmup, args.iters)
|
||
results.append(row)
|
||
|
||
spd = row["fragmented_step_ms"] / max(row["fused_step_ms"], 0.01)
|
||
print(f"{E:>7} | {row['fragmented_n_params']:>7} {row['fused_n_params']:>6} | "
|
||
f"{row['fragmented_n_elements']:>12,} | "
|
||
f"{row['fragmented_step_ms']:>9.1f}ms {row['fused_step_ms']:>9.1f}ms {spd:>7.1f}x | "
|
||
f"{row['fragmented_clip_ms']:>9.1f}ms {row['fused_clip_ms']:>9.1f}ms")
|
||
|
||
all_results[label] = results
|
||
print()
|
||
|
||
# Save
|
||
output = {
|
||
"config": {
|
||
"hidden_size": H, "intermediate_size": I, "lora_rank": r,
|
||
"num_layers": L, "torch_threads": threads,
|
||
"warmup": args.warmup, "iters": args.iters,
|
||
"torch_version": torch.__version__,
|
||
},
|
||
"results_foreach": all_results.get("foreach", []),
|
||
"results_no_foreach": all_results.get("no-foreach", []),
|
||
}
|
||
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||
"bench_optimizer_step_results.json")
|
||
with open(out_path, "w") as f:
|
||
json.dump(output, f, indent=2)
|
||
print(f"Results saved to {out_path}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
run_benchmark()
|