- Extract model loading from generate()/edit() into VnAssetsSession class - Session eagerly loads SDXL + Qwen Image Edit at construction (28s, once) - Both models held in GPU memory across calls; generate()/edit() reuse them - generate.py and edit.py become thin wrappers (backwards compatible CLI) - Context manager (with VnAssetsSession(...) as vna:) for library use - Metadata backwards-compatible: all fields preserved including lora_load_s - load_time_s now reflects total session construction, amortized across calls - Add performance stats for edit path (Qwen Image Edit + Lightning LoRA) - Benchmark matmul fallback (86.8s) vs flash attention (53.3s, 1.63x speedup) - Session vs cold start comparison: 2 ops save one 28s load, 5 edits save 112s - Flash attention via TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 documented
137 lines
4.3 KiB
Python
137 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Deep batch test: generate base image, edit with multiple emotions, track timing."""
|
|
import subprocess
|
|
import time
|
|
import json
|
|
from pathlib import Path
|
|
|
|
OUTPUT_DIR = Path("output/batch_test")
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
BASE_PROMPT = "1girl, solo, red hair, glasses, blue eyes, white crop top, standing, portrait"
|
|
NEG_PROMPT = "deformed, ugly, bad quality, lowres"
|
|
CHECKPOINT = "models/novaAnimeXL_ilV190.safetensors"
|
|
EDIT_MODEL = "models/qwen_image_edit_2509_fp8_e4m3fn.safetensors"
|
|
SEED = 42
|
|
STEPS = 20
|
|
CFG = 4.5
|
|
|
|
# Emotion variants to edit
|
|
EMOTIONS = [
|
|
("smile", "make her smile happily with a warm genuine smile"),
|
|
("angry", "make her look angry and furious, furrowed brow"),
|
|
("sad", "make her look sad and crying, tears in her eyes"),
|
|
("surprised", "make her look surprised, wide eyes, mouth slightly open"),
|
|
("blushing", "make her blush intensely, embarrassed expression, pink cheeks"),
|
|
]
|
|
|
|
results = []
|
|
|
|
# ── 1. Generate base image ──────────────────────────────────────
|
|
print("=" * 60)
|
|
print("STEP 1: Generate base image")
|
|
print("=" * 60)
|
|
|
|
base_path = OUTPUT_DIR / "base.png"
|
|
t_start = time.perf_counter()
|
|
|
|
subprocess.run([
|
|
"vnasset", "generate",
|
|
"--checkpoint", CHECKPOINT,
|
|
"--prompt", BASE_PROMPT,
|
|
"--negative-prompt", NEG_PROMPT,
|
|
"--steps", str(STEPS),
|
|
"--cfg", str(CFG),
|
|
"--seed", str(SEED),
|
|
"--output", str(base_path),
|
|
], check=True)
|
|
|
|
t_gen = time.perf_counter() - t_start
|
|
|
|
# Read metadata
|
|
meta = json.loads((OUTPUT_DIR / "base.json").read_text())
|
|
results.append({
|
|
"step": "generate_base",
|
|
"output": str(base_path),
|
|
"prompt": BASE_PROMPT,
|
|
"load_s": meta["load_time_s"],
|
|
"inference_s": meta["inference_time_s"],
|
|
"total_wall_s": round(t_gen, 1),
|
|
})
|
|
print(f" Wall time: {t_gen:.1f}s (load: {meta['load_time_s']}s, inference: {meta['inference_time_s']}s)\n")
|
|
|
|
# ── 2. Edit with each emotion ───────────────────────────────────
|
|
total_edits_start = time.perf_counter()
|
|
|
|
for i, (name, prompt) in enumerate(EMOTIONS):
|
|
print("=" * 60)
|
|
print(f"STEP {i+2}: Edit → {name}")
|
|
print("=" * 60)
|
|
|
|
edit_path = OUTPUT_DIR / f"base_{name}.png"
|
|
|
|
t_start = time.perf_counter()
|
|
subprocess.run([
|
|
"vnasset", "edit",
|
|
"--model", EDIT_MODEL,
|
|
"--input", str(base_path),
|
|
"--prompt", prompt,
|
|
"--steps", str(STEPS),
|
|
"--cfg", str(CFG),
|
|
"--seed", str(SEED),
|
|
"--output", str(edit_path),
|
|
], check=True)
|
|
t_edit = time.perf_counter() - t_start
|
|
|
|
meta = json.loads((OUTPUT_DIR / f"base_{name}.json").read_text())
|
|
results.append({
|
|
"step": f"edit_{name}",
|
|
"output": str(edit_path),
|
|
"prompt": prompt,
|
|
"load_s": meta["load_time_s"],
|
|
"inference_s": meta["inference_time_s"],
|
|
"total_wall_s": round(t_edit, 1),
|
|
})
|
|
print(f" Wall time: {t_edit:.1f}s (load: {meta['load_time_s']}s, inference: {meta['inference_time_s']}s)\n")
|
|
|
|
total_edits_wall = time.perf_counter() - total_edits_start
|
|
|
|
# ── 3. Summary ──────────────────────────────────────────────────
|
|
print("=" * 60)
|
|
print("SUMMARY")
|
|
print("=" * 60)
|
|
print(f"{'Step':<20} {'Load':>8} {'Infer':>8} {'Wall':>8}")
|
|
print("-" * 48)
|
|
total_load = 0
|
|
total_infer = 0
|
|
total_wall = 0
|
|
for r in results:
|
|
print(f"{r['step']:<20} {r['load_s']:>7.1f}s {r['inference_s']:>7.1f}s {r['total_wall_s']:>7.1f}s")
|
|
total_load += r["load_s"]
|
|
total_infer += r["inference_s"]
|
|
total_wall += r["total_wall_s"]
|
|
print("-" * 48)
|
|
print(f"{'TOTAL':<20} {total_load:>7.1f}s {total_infer:>7.1f}s {total_wall:>7.1f}s")
|
|
|
|
# Write results JSON
|
|
summary = {
|
|
"config": {
|
|
"checkpoint": CHECKPOINT,
|
|
"edit_model": EDIT_MODEL,
|
|
"seed": SEED,
|
|
"steps": STEPS,
|
|
"cfg": CFG,
|
|
"base_prompt": BASE_PROMPT,
|
|
},
|
|
"results": results,
|
|
"totals": {
|
|
"load_s": round(total_load, 1),
|
|
"inference_s": round(total_infer, 1),
|
|
"total_wall_s": round(total_wall, 1),
|
|
},
|
|
}
|
|
summary_path = OUTPUT_DIR / "summary.json"
|
|
summary_path.write_text(json.dumps(summary, indent=2))
|
|
print(f"\nSummary saved to {summary_path}")
|
|
print(f"Images in {OUTPUT_DIR}/")
|