Add VnAssetsSession for persistent model lifecycle

- 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
This commit is contained in:
Michele Rossi
2026-07-08 10:18:42 +02:00
parent e7cde842b3
commit 97ac841518
8 changed files with 1072 additions and 212 deletions

136
test_batch.py Normal file
View File

@@ -0,0 +1,136 @@
#!/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}/")