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

348 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()