项目文件夹

文件
2026-07-13 12:34:46 +08:00

348 行
12 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env python3
"""
End-to-end benchmark on REAL trajectory data:
1. GPU FP16 baseline
2. GPU W8A16 (native quantized_matmul)
3. GPU W8A8_pg (INT8 TensorOps via cider, pergroup)
4. GPU W8A8_pc (INT8 TensorOps via cider, perchannel)
5-8. Above 4 configs + Cider SDPA patch (decode acceleration)
Uses replay_prompt.build_prompt_at_step() to build real prompts with real screenshots.
Compares both accuracy (action output) and speed (prefill + decode).
"""
import sys, os, time, gc, re, json, io, base64
import numpy as np
import mlx.core as mx
import mlx.nn as nn
from PIL import Image
from pathlib import Path
ROOT_DIR = str(Path(__file__).parent.parent)
sys.path.insert(0, ROOT_DIR)
sys.path.insert(0, os.path.join(ROOT_DIR, "vlm_service"))
from session_data.replay_prompt import build_prompt_at_step
from vlm_service.custom_qwen3vl import custom_stream_generate
from mlx_vlm.utils import load as vlm_load
home_dir = os.path.expanduser("~")
FP16_MODEL = os.path.join(home_dir, 'Downloads/checkpoint-50349')
W8A16_MODEL = os.path.join(home_dir, 'Downloads/checkpoint-50349-mlx_w8a16')
SESSION_DIR = os.path.join(ROOT_DIR, "session_data")
STEPS = [0, 1, 2] # step 0: 1img, step 1: 2imgs, step 2: 3imgs
MAX_TOKENS = 200
PREFILL_STEP = 8192
N_SPEED_RUNS = 2 # per-step speed runs
def load_step(session_dir, step):
"""Load prompt + images for a given step."""
data = build_prompt_at_step(session_dir, step)
pil_images = [Image.open(io.BytesIO(base64.b64decode(b))) for b in data['images']]
return data, pil_images
def build_messages(data):
"""Build chat messages from replay_prompt data, with <image> placeholders."""
prompt_text = data['prompt']
n_images = prompt_text.count('<image>')
parts = prompt_text.split('<image>')
content = []
for i, part in enumerate(parts):
if part:
content.append({"type": "text", "text": part})
if i < n_images:
content.append({"type": "image"})
messages = [
{"role": "system", "content": data['system_prompt']},
{"role": "user", "content": content},
]
return messages
def run_generate(model, processor, messages, pil_images):
"""Run one generation, return (text, timing_dict)."""
prompt = processor.tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
gc.collect()
mx.clear_cache()
mx.reset_peak_memory()
text = ""
prefill_time = 0
decode_tps = 0
prompt_tokens = 0
gen_tokens = 0
try:
for resp in custom_stream_generate(
model, processor,
prompt=prompt,
image=pil_images,
max_tokens=MAX_TOKENS,
temperature=0.0,
prefill_step_size=PREFILL_STEP,
verbose=False,
):
text += resp.text
prefill_time = resp.prompt_tokens / resp.prompt_tps if resp.prompt_tps > 0 else 0
decode_tps = resp.generation_tps
prompt_tokens = resp.prompt_tokens
gen_tokens = resp.generation_tokens
except UnicodeDecodeError:
# mlx_vlm detokenizer sometimes fails on partial utf-8 sequences
pass
total_time = prefill_time + (gen_tokens / decode_tps if decode_tps > 0 else 0)
return text, {
'prefill_ms': prefill_time * 1000,
'prefill_tps': prompt_tokens / prefill_time if prefill_time > 0 else 0,
'decode_tps': decode_tps,
'prompt_tokens': prompt_tokens,
'gen_tokens': gen_tokens,
'total_ms': total_time * 1000,
}
def extract_action(text):
"""Extract action from model output."""
m = re.search(r'<action>(.*?)</action>', text, re.DOTALL)
return m.group(1).strip() if m else None
def extract_coords(action_str):
"""Extract (x, y) from action string."""
if not action_str:
return None
nums = re.findall(r'\d+', action_str)
if len(nums) >= 2:
return (int(nums[0]), int(nums[1]))
return None
def main():
print("=" * 80)
print(" E2E Benchmark: FP16 / W8A16 / W8A8 × MLX SDPA / Cider SDPA")
print("=" * 80)
print(f" Session: {SESSION_DIR}")
print(f" Steps: {STEPS}")
print(f" Max tokens: {MAX_TOKENS}")
print(f" Speed runs per step: {N_SPEED_RUNS}")
# Load all steps
print("\nLoading trajectory data...")
step_data = {}
for s in STEPS:
data, images = load_step(SESSION_DIR, s)
msgs = build_messages(data)
step_data[s] = (data, images, msgs)
print(f" Step {s}: {len(images)} images")
# Import cider for SDPA patching
import cider
# configs: (key, model_path, use_cider_quant, use_cider_sdpa, label)
configs = [
# Without Cider SDPA (MLX default attention)
('fp16', FP16_MODEL, False, False, "FP16"),
('fp16+sdpa', FP16_MODEL, False, True, "FP16+CiderSDPA"),
('w8a16', W8A16_MODEL, False, False, "W8A16"),
('w8a16+sdpa', W8A16_MODEL, False, True, "W8A16+CiderSDPA"),
('w8a8_pg', W8A16_MODEL, True, False, "W8A8_pg"),
('w8a8_pg+sdpa', W8A16_MODEL, True, True, "W8A8_pg+CiderSDPA"),
('w8a8_pc', FP16_MODEL, True, False, "W8A8_pc"),
('w8a8_pc+sdpa', FP16_MODEL, True, True, "W8A8_pc+CiderSDPA"),
]
all_results = {}
for cfg_key, model_path, use_cider_quant, use_cider_sdpa, label in configs:
print(f"\n{'='*80}")
print(f" {label}")
print(f"{'='*80}")
# Load model
print(f" Loading {model_path}...")
model, proc = vlm_load(model_path)
# Apply cider quantization if needed
if use_cider_quant:
from cider import convert_model
stats = convert_model(model)
print(f" [cider quant] {stats}")
# Apply cider SDPA if needed
if use_cider_sdpa:
cider.patch_sdpa(verbose=True)
else:
cider.unpatch_sdpa(verbose=False)
cfg_results = {}
for s in STEPS:
data, images, msgs = step_data[s]
print(f"\n Step {s} ({len(images)} imgs):")
# Warmup
text, timing = run_generate(model, proc, msgs, images)
print(f" Warmup: {timing['prompt_tokens']} prompt tok, "
f"prefill {timing['prefill_ms']:.0f}ms "
f"({timing['prefill_tps']:.0f} tok/s), "
f"decode {timing['decode_tps']:.1f} tok/s")
# Accuracy run (use warmup result)
action = extract_action(text)
coords = extract_coords(action)
# Speed runs
timings = []
for r in range(N_SPEED_RUNS):
_, t = run_generate(model, proc, msgs, images)
timings.append(t)
print(f" Run {r+1}: prefill {t['prefill_ms']:.0f}ms "
f"({t['prefill_tps']:.0f} tok/s) | "
f"decode {t['decode_tps']:.1f} tok/s | "
f"total {t['total_ms']:.0f}ms")
cfg_results[s] = {
'text': text,
'action': action,
'coords': coords,
'timings': timings,
'prompt_tokens': timings[0]['prompt_tokens'],
}
print(f" Action: {action}")
all_results[cfg_key] = cfg_results
# Free model before loading next
del model, proc
gc.collect()
mx.clear_cache()
# Ensure SDPA is unpatched after benchmark
cider.unpatch_sdpa(verbose=False)
# ── Speed Summary ──
print(f"\n{'='*80}")
print(f" SPEED SUMMARY (median of {N_SPEED_RUNS} runs)")
print(f"{'='*80}")
cfg_keys = [c[0] for c in configs]
cfg_labels = {c[0]: c[4] for c in configs}
# Table: per-step results
print(f"\n {'Config':<22s}", end="")
for s in STEPS:
print(f" | Step{s} prefill decode total", end="")
print()
print(f" {'─'*22}", end="")
for _ in STEPS:
print(f" | {'─'*30}", end="")
print()
for k in cfg_keys:
line = f" {cfg_labels[k]:<22s}"
for s in STEPS:
ts = all_results[k][s]['timings']
pf = float(np.median([t['prefill_tps'] for t in ts]))
dc = float(np.median([t['decode_tps'] for t in ts]))
tt = float(np.median([t['total_ms'] for t in ts]))
line += f" | {pf:>7.0f}t/s {dc:>5.1f}t/s {tt:>5.0f}ms"
print(line)
# ── Decode speed comparison (SDPA impact) ──
print(f"\n{'='*80}")
print(f" DECODE SPEED: MLX SDPA vs Cider SDPA")
print(f"{'='*80}")
# Group by base config
base_configs = ['fp16', 'w8a16', 'w8a8_pg', 'w8a8_pc']
for base in base_configs:
sdpa_key = base + '+sdpa'
if base not in all_results or sdpa_key not in all_results:
continue
print(f"\n {cfg_labels[base]}:")
for s in STEPS:
ts_base = all_results[base][s]['timings']
ts_sdpa = all_results[sdpa_key][s]['timings']
dc_base = float(np.median([t['decode_tps'] for t in ts_base]))
dc_sdpa = float(np.median([t['decode_tps'] for t in ts_sdpa]))
speedup = dc_sdpa / dc_base if dc_base > 0 else 0
pf_base = float(np.median([t['prefill_tps'] for t in ts_base]))
pf_sdpa = float(np.median([t['prefill_tps'] for t in ts_sdpa]))
print(f" Step {s}: decode {dc_base:.1f}{dc_sdpa:.1f} tok/s "
f"({speedup:.2f}x) | prefill {pf_base:.0f}{pf_sdpa:.0f} tok/s")
# ── Accuracy comparison ──
print(f"\n{'='*80}")
print(f" ACCURACY: SDPA patch should not change outputs")
print(f"{'='*80}")
for base in base_configs:
sdpa_key = base + '+sdpa'
if base not in all_results or sdpa_key not in all_results:
continue
mismatches = 0
for s in STEPS:
a1 = all_results[base][s]['action']
a2 = all_results[sdpa_key][s]['action']
if a1 != a2:
mismatches += 1
c1 = all_results[base][s]['coords']
c2 = all_results[sdpa_key][s]['coords']
if c1 and c2:
diff = (abs(c1[0]-c2[0]), abs(c1[1]-c2[1]))
close = all(d <= 5 for d in diff)
tag = f"{'≈' if close else '≠'} diff=({diff[0]},{diff[1]})"
else:
tag = "≠"
print(f" {cfg_labels[base]} Step {s}: {tag}")
print(f" base: {a1}")
print(f" sdpa: {a2}")
if mismatches == 0:
print(f" {cfg_labels[base]}: all {len(STEPS)} steps IDENTICAL ✅")
# ── Overall Summary ──
print(f"\n{'='*80}")
print(f" OVERALL DECODE tok/s (median across all steps)")
print(f"{'='*80}")
for k in cfg_keys:
all_decode = []
for s in STEPS:
ts = all_results[k][s]['timings']
all_decode.append(float(np.median([t['decode_tps'] for t in ts])))
med = np.median(all_decode)
print(f" {cfg_labels[k]:<22s}: {med:.1f} tok/s")
# Save
out_path = '/tmp/e2e_wxa16_sdpa_results.json'
save_data = {}
for k in cfg_keys:
save_data[k] = {}
for s in STEPS:
r = all_results[k][s]
save_data[k][str(s)] = {
'action': r['action'],
'coords': r['coords'],
'prompt_tokens': r['prompt_tokens'],
'timings_median': {
'prefill_ms': float(np.median([t['prefill_ms'] for t in r['timings']])),
'prefill_tps': float(np.median([t['prefill_tps'] for t in r['timings']])),
'decode_tps': float(np.median([t['decode_tps'] for t in r['timings']])),
'total_ms': float(np.median([t['total_ms'] for t in r['timings']])),
},
}
with open(out_path, 'w') as f:
json.dump(save_data, f, indent=2)
print(f"\n Results saved to {out_path}")
print(f"{'='*80}")
if __name__ == '__main__':
main()