Repository navigation
v2.4.4 - v2.2.7 Duplicate File I/O Elimination — Loss and Restoration
| EN | 中文 |
Overview
Documents the fact that the "first LoRA duplicate file read elimination" feature added in v2.2.7 (2026-01-07) was unintentionally lost in the v2.3.0 (2026-01-22) AWQ modulation layer LoRA fix commit, and subsequently restored.
1. Feature Added in v2.2.7
Commit: 1ff4db9
Release: v2.2.7 — LoRA Loading Speed Improvement: Duplicate File Read Elimination
Code Added
# OPTIMIZATION: Cache first LoRA state dict for reuse in processing loop
_cached_first_lora_state_dict = None
if lora_configs:
first_lora_path_or_dict, first_lora_strength = lora_configs[0]
first_lora_state_dict = _load_lora_state_dict(first_lora_path_or_dict)
_cached_first_lora_state_dict = first_lora_state_dict # Cache for reuse
# 1. Aggregate weights from all LoRAs
for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):
lora_name = lora_path_or_dict if isinstance(lora_path_or_dict, str) else "dict"
# OPTIMIZATION: Reuse cached first LoRA state dict to avoid duplicate file I/O
if idx == 0 and _cached_first_lora_state_dict is not None:
lora_state_dict = _cached_first_lora_state_dict
else:
lora_state_dict = _load_lora_state_dict(lora_path_or_dict)Impact and Significance
Why the first LoRA was read twice
compose_loras_v2() loaded the file in two separate stages:
-
Debug logging stage (lines 1199-1212)
first_lora_state_dict = _load_lora_state_dict(first_lora_path_or_dict) for key in first_lora_state_dict.keys(): parsed_res = _classify_and_map_key(key) logger.info(...) # ← first_lora_state_dict is discarded as a local variable
-
Actual processing loop stage (lines 1217-1223)
for lora_path_or_dict, strength in lora_configs: lora_state_dict = _load_lora_state_dict(lora_path_or_dict) # ← Same file re-read
The state_dict loaded for debug logging (hundreds of MB to several GB) went out of scope and became eligible for garbage collection. The actual processing loop then re-read the same file from disk, causing entirely wasted work.
Quantified waste
| Operation | Before fix | After fix | Reduction |
|---|---|---|---|
| File I/O | 2 times | 1 time | 50% |
| Binary parse & tensor deserialization | 2 times | 1 time | 50% |
Key classification (_classify_and_map_key) |
2×N times | 1×N times | 50% |
N = number of keys in the LoRA file (typically hundreds to thousands)
Concrete performance impact
- 500MB LoRA file: ~1-3 seconds saved (depending on disk speed)
- 2GB LoRA file: ~5-10 seconds saved
- User perception: Reduced wait time from prompt execution to generation start
Why this matters
-
Single LoRA is the most common use case
- Many workflows apply only one LoRA
- In that case, the "first LoRA" is the only LoRA, so waste is 100%
-
File I/O is a synchronous block
comfy.utils.load_torch_file()runs synchronously; downstream processing halts until it completes- The UI thread is blocked, making the ComfyUI console appear "frozen"
-
Deserialization is CPU-intensive
- Binary parsing in safetensors / torch.load places heavy CPU load
- Especially noticeable with large LoRAs (FLUX, SDXL, Qwen-class)
-
Key classification is a chain of regex matching
_classify_and_map_key()runs ~30 regex patterns against each key- Cost scales linearly with the number of keys
Cache safety
_cached_first_lora_state_dictis a local variable insidecompose_loras_v2- Automatically discarded when the function exits; no state leaks across calls
state_dictis treated as read-only; debug logging does not mutate it- The
idx == 0condition ensures the second and subsequent LoRAs are unaffected
2. Preserved in v2.2.8
Commit: c113968
Release: v2.2.8 — Fix for Issue #44: Console Slowdown When Loading Unsupported LoRA Formats
Issue #44 (massive log output for unsupported LoRA formats) was fixed, but the _cached_first_lora_state_dict cache mechanism was preserved as-is.
git show c113968:nunchaku_code/lora_qwen.py | Select-String "cached_first_lora"
→ _cached_first_lora_state_dict = None
→ _cached_first_lora_state_dict = first_lora_state_dict
→ if idx == 0 and _cached_first_lora_state_dict is not None:
3. Lost in v2.3.0
Commit: 6498929
Release: v2.3.0 — Implement AWQ modulation layer LoRA fix with Runtime Monkey Patch
Change scope: nunchaku_code/lora_qwen.py | 865 ++++++++++++++--------------- | 266 insertions(+), 599 deletions(-)
Lost code (confirmed via git diff)
- # OPTIMIZATION: Cache first LoRA state dict for reuse in processing loop
- _cached_first_lora_state_dict = None
- if lora_configs:
- first_lora_path_or_dict, first_lora_strength = lora_configs[0]
- first_lora_state_dict = _load_lora_state_dict(first_lora_path_or_dict)
- _cached_first_lora_state_dict = first_lora_state_dict # Cache for reuse
- # 1. Aggregate weights from all LoRAs
- for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):
- lora_name = lora_path_or_dict if isinstance(lora_path_or_dict, str) else "dict"
- # OPTIMIZATION: Reuse cached first LoRA state dict to avoid duplicate file I/O
- if idx == 0 and _cached_first_lora_state_dict is not None:
- lora_state_dict = _cached_first_lora_state_dict
- else:
- lora_state_dict = _load_lora_state_dict(lora_path_or_dict)
+ # --- 4. Main Loading Loop ---
+ for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):
+ if abs(strength) < 1e-5:
+ continueCause of loss
A migration omission during the sectional restructuring of compose_loras_v2().
v2.2.8 structure:
Cache init → Debug logging → Loop (idx == 0 cache reuse)
v2.3.0 structure:
# --- 1. Format Detection ---
# --- 2. Early Exit ---
# --- 3. Debug Inspection (First LoRA) ---
# --- 4. Main Loading Loop ---
In Section 3, first_lora_state_dict is loaded but remains a local variable inaccessible from Section 4. Section 4 calls _load_lora_state_dict_robust(lora_path_or_dict) unconditionally, so the first LoRA is read twice.
enumerate(lora_configs) remained, but all three elements of the cache variable (declaration, save, and reuse) disappeared.
4. Code at v2.4.3
# --- 3. Debug Inspection (First LoRA) ---
if lora_configs:
first_lora_path_or_dict, first_lora_strength = lora_configs[0]
first_lora_state_dict = _load_lora_state_dict_robust(first_lora_path_or_dict)
_first_detection = _detect_lora_format(first_lora_state_dict)
# --- 4. Main Loading Loop ---
for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):
...
lora_state_dict = _load_lora_state_dict_robust(lora_path_or_dict) # Second read at idx==0Duplicate file I/O had reappeared.
5. Compatibility with AWQ Monkey Patch
The AWQ modulation layer LoRA fix (_awq_lora_forward) operates inside _apply_lora_to_module. This is at the tail end of the compose_loras_v2 flow: file load → weight processing → module application.
File-load optimization (caching) sits at the entry point of this flow. The two are independent layers and can coexist:
_cached_first_lora_state_dict = None # Added
if lora_configs:
first_lora_path_or_dict, first_lora_strength = lora_configs[0]
first_lora_state_dict = _load_lora_state_dict_robust(first_lora_path_or_dict)
_cached_first_lora_state_dict = first_lora_state_dict # Added
_first_detection = _detect_lora_format(first_lora_state_dict)
for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):
...
if idx == 0 and _cached_first_lora_state_dict is not None: # Added
lora_state_dict = _cached_first_lora_state_dict
else:
lora_state_dict = _load_lora_state_dict_robust(lora_path_or_dict)This change has zero impact on AWQ monkey-patch code (the should_apply_awq_mod check in Section 2, the AWQ branch inside _apply_lora_to_module). The cache mechanism and the AWQ patch are independent layers.
6. Facts of the Migration Omission
Changes actually made in commit 6498929
nunchaku_code/lora_qwen.py | 865 ++++++++++++++--------------- | 266 insertions(+), 599 deletions(-)
- 599 lines deleted: A massive rewrite of the
compose_loras_v2function structure. - 266 lines added: Mostly AWQ monkey-patch code (
_awq_lora_forward,DEFAULT_APPLY_AWQ_MOD_ENV, etc.).
How the structure changed
v2.2.8 and earlier (sequential processing):
def compose_loras_v2(...):
# Initialize cache variable
_cached_first_lora_state_dict = None
# Load first LoRA for debug logging
if lora_configs:
first_lora = _load_lora_state_dict(...)
_cached_first_lora_state_dict = first_lora # Save to cache
# Main loop (reuse cache)
for idx, (lora_path, strength) in enumerate(lora_configs):
if idx == 0 and _cached_first_lora_state_dict is not None:
lora_state_dict = _cached_first_lora_state_dict # Reuse
else:
lora_state_dict = _load_lora_state_dict(lora_path)v2.3.0 and later (sectioned):
def compose_loras_v2(...):
# --- 1. Z-Image / NextDiT Handling ---
# --- 2. Environment Variable / Argument Logic for AWQ Mod ---
# --- 3. Debug Inspection (First LoRA) ---
if lora_configs:
first_lora = _load_lora_state_dict_robust(...)
# ← Loaded here, but not saved to a cache variable
# --- 4. Main Loading Loop ---
for idx, (lora_path, strength) in enumerate(lora_configs):
lora_state_dict = _load_lora_state_dict_robust(lora_path)
# ← Re-read at idx==0. No cache variable exists, so no branch is possible.What disappeared (all three elements)
| Element | v2.2.8 | v2.3.0 |
|---|---|---|
| Cache variable declaration | _cached_first_lora_state_dict = None |
Deleted |
| Save to cache | _cached_first_lora_state_dict = first_lora_state_dict |
Deleted |
| Cache reuse | if idx == 0 and _cached_first_lora_state_dict is not None: |
Deleted |
What remained
for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):enumerate remained. It looks like a trace of the optimization, but because the idx == 0 branch logic is gone, enumerate functions only as a counter and is never used for cache reuse.
Facts from the commit message
- Commit message:
Implement AWQ modulation layer LoRA fix with Runtime Monkey Patch - The body contains no mention of "cache deletion," "optimization removal," or "file I/O changes."
- The purpose was to add the AWQ monkey patch; there was no intent to discard the existing file I/O optimization.
Conclusion
When restructuring compose_loras_v2 into sections in commit 6498929, the developer kept enumerate(lora_configs) but forgot to port the three elements of _cached_first_lora_state_dict (declaration, save, and reuse) into the new structure. The commit's purpose was to add the AWQ monkey patch; removal of the file I/O optimization was not the intent.
7. Restoration Facts
File modified
nunchaku_code/lora_qwen.py
Code added
Section 3 (Debug Inspection):
# --- 3. Debug Inspection (First LoRA) ---
# OPTIMIZATION: Cache first LoRA state dict for reuse in processing loop
_cached_first_lora_state_dict = None
if lora_configs:
first_lora_path_or_dict, first_lora_strength = lora_configs[0]
first_lora_state_dict = _load_lora_state_dict_robust(first_lora_path_or_dict)
_cached_first_lora_state_dict = first_lora_state_dict # Cache for reuseSection 4 (Main Loading Loop):
# OPTIMIZATION: Reuse cached first LoRA state dict to avoid duplicate file I/O
if idx == 0 and _cached_first_lora_state_dict is not None:
lora_state_dict = _cached_first_lora_state_dict
else:
lora_state_dict = _load_lora_state_dict_robust(lora_path_or_dict)Significance
Restored the v2.2.7 "first LoRA duplicate read elimination" feature in full compatibility with the AWQ modulation layer monkey patch (v2.3.0).
- Section 3 saves the loaded
first_lora_state_dictinto_cached_first_lora_state_dict - Section 4 reuses that cache at
idx == 0, avoiding a second_load_lora_state_dict_robustcall - Achieves a 50% reduction in file I/O, binary parsing, and key classification
This fix has zero impact on the AWQ monkey patch (_awq_lora_forward, the AWQ branch inside _apply_lora_to_module). The cache mechanism lives in the file-loading layer; the AWQ patch lives in the module-application layer. They are completely independent.