4.0 KiB
Design: AutoTuner Diff Cache
Problem
AutoTuner Phase 2 evaluates top-N candidate configs by running a full optimize_merge() for each. Every candidate recomputes raw LoRA diffs (A@B matmul) from scratch in _merge_one_prefix() (lines 3822-3832), even though diffs depend only on LoRA content — not merge config.
For 3 candidates × 500 prefixes × 3 LoRAs = 4500 redundant matmuls.
Solution
Cache raw diffs after the first candidate's merge, reuse for subsequent candidates. The cache stores diff[prefix][lora_index] → tensor on CPU (or disk via mmap).
What's Cacheable
| Component | Cacheable | Reason |
|---|---|---|
| Raw diffs (mat_up @ mat_down × alpha/rank) | Yes | Pure function of LoRA matrices |
| Target shapes per prefix | Yes | Constant |
| LoRA effective strengths | Yes | Same stack across candidates |
| TIES trim masks | No | Depends on density param |
| DARE/DELLA masks | No | Depends on density + dampening |
| Merge algorithm output | No | Different mode per candidate |
| Quality enhancements (KnOTS SVD) | No | Depends on quality level |
Architecture
New Parameter on AutoTuner Node
diff_cache_mode: combo with options:
"ram"(default) — cache diffs in a dict on CPU"disk"— cache diffs as memory-mapped temp files"disabled"— no caching (current behavior)
Cache Lifecycle
- First candidate merge → compute diffs normally, store in cache
- Subsequent candidates → retrieve diffs from cache, skip A@B
- After Phase 2 completes → clear cache (RAM) or delete temp dir (disk)
Implementation
RAM Mode
# In AutoTuner, before Phase 2 loop:
self._diff_cache = {} # key: (lora_prefix, lora_index) → CPU tensor
# In _merge_one_prefix, at diff computation (line 3822):
cache_key = (lora_prefix, i)
if self._diff_cache is not None and cache_key in self._diff_cache:
diff = self._diff_cache[cache_key].to(device)
else:
diff = torch.mm(mat_up..., mat_down...) * (alpha / rank)
if self._diff_cache is not None:
self._diff_cache[cache_key] = diff.cpu()
Disk Mode
import tempfile
# Before Phase 2:
self._diff_cache_dir = tempfile.mkdtemp(prefix="lora_diff_cache_")
# Store: torch.save(diff.cpu(), os.path.join(dir, f"{prefix}_{i}.pt"))
# Load: diff = torch.load(path, map_location=device, mmap=True)
torch.load(..., mmap=True) (PyTorch 2.1+) memory-maps the file, so tensors are paged in on demand without consuming RSS.
Memory Estimates
| Model | Prefixes | Avg Diff Size | 3 LoRAs | RAM |
|---|---|---|---|---|
| SD 1.5 | ~300 | ~0.5MB | 450MB | Trivial |
| SDXL | ~500 | ~2MB | 3GB | Fine |
| Flux | ~1000 | ~4MB | 12GB | Needs 32GB+ system |
Where to Modify
- AutoTuner node INPUT_TYPES — add
diff_cache_modecombo - AutoTuner Phase 2 loop (~line 4492) — init cache before loop, clear after
_merge_one_prefix(~line 3822) — check cache before computing diffoptimize_merge— thread the cache object through (add_diff_cacheparam alongside existing_analysis_cache)
Cleanup
# After Phase 2 (line ~4570):
if self._diff_cache is not None:
self._diff_cache.clear()
self._diff_cache = None
if self._diff_cache_dir:
shutil.rmtree(self._diff_cache_dir, ignore_errors=True)
Expected Speedup
For top_n=3: eliminates 2/3 of A@B matmuls in Pass 2. Since diff computation is a significant fraction of merge time (especially on CPU), this should reduce Phase 2 wall time by 30-50%.
For top_n=5+: eliminates 4/5 of matmuls → 50-70% reduction.
Risks
- RAM pressure: 12GB for Flux. Mitigated by disk mode option.
- Cache invalidation: Not needed — cache lives only for one AutoTuner run, cleared after Phase 2.
- Thread safety:
_merge_one_prefixis called from ThreadPoolExecutor on CPU mode. Dict reads are thread-safe in CPython (GIL). Writes need care — first candidate populates, subsequent candidates read. Since candidates run sequentially (line 4492), this is safe.