Files
pollockjj-ComfyUI-MultiGPU/memory-bank/systemPatterns.md
T
John Pollock 429be7c912 docs: update activeContext for v2.5.0 release with refactoring summary
Update active context documentation to reflect v2.5.0 release candidate status.
Major achievements documented:

- DisTorch2 allocation refactoring (-179 lines, unified UNET/CLIP logic)
- Production cleanup removing debug instrumentation (-40 lines)
- Verified selective unload system working with production logs
- Architecture status showing all core files production-ready
- Updated memory management pipeline with verification details

Reorganized content to prioritize recent session achievements (2025-09-30)
and production readiness status. Total code reduction: 219 lines through
consolidation and cleanup while maintaining full functionality.
2025-09-30 09:07:05 -05:00

467 lines
16 KiB
Markdown
Raw 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.
# System Architecture & Patterns (Updated 2025-09-29)
## Core Architecture
### Dynamic Class Override System
Foundation Pattern: City96's elegant inheritance-based approach (Dec 2024 revolution)
```python
def override_class(original_class, device_param="device"):
class MultiGPUClass(original_class):
@classmethod
def INPUT_TYPES(cls):
inputs = original_class.INPUT_TYPES()
inputs["required"][device_param] = (get_device_list(),)
return inputs
def override(self, *args, **kwargs):
device = kwargs.pop(device_param, None)
mm.text_encoder_device = device
return original_class.FUNCTION(self, *args, **kwargs)
return MultiGPUClass
```
Key Benefits:
- 50 lines vs 400+: Eliminated manual class definitions
- Universal Support: Works with any ComfyUI loader node
- Maintenance: Auto-adapts to ComfyCore changes
- Consistency: Unified behavior across all MultiGPU nodes
### Load-Patch-Distribute (LPD) Method
DisTorch2 Core Process:
```python
# 1. LOAD - Always on compute device first
tensor = load_tensor_on_compute_device(tensor_name)
# 2. PATCH - Apply all LoRAs at full precision
if lora_patches:
tensor = apply_lora_patches(tensor, lora_patches, precision=torch.float16)
# 3. DISTRIBUTE - Move to target device after patching
final_tensor = tensor.to(target_device)
```
Design Principles:
- Quality First: No precision loss during LoRA application
- Deterministic: Same allocation every time
- ComfyUI Native: Works with existing ComfyCore patterns
## Memory Management Architecture
### Virtual VRAM System
Concept: Make CPU/secondary GPU memory appear as extended VRAM
```python
class VirtualVRAM:
def __init__(self, compute_device, donor_device, virtual_gb):
self.compute_device = compute_device # e.g., "cuda:0"
self.donor_device = donor_device # e.g., "cpu" or "cuda:1"
self.virtual_gb = virtual_gb # Extended memory pool
def allocate_layers(self, model_layers, allocation_string):
# "cuda:0,2.5gb;cpu,*" -> assign layers based on cumulative memory
```
### Expert Allocation Modes
Bytes Mode (Recommended):
```python
# "cuda:0,2.5gb;cuda:1,3.0g;cpu,*"
def parse_bytes_allocation(allocation_string):
devices = []
for device_spec in allocation_string.split(';'):
device_name, memory_spec = device_spec.split(',')
if memory_spec == '*':
memory_bytes = float('inf') # Overflow device
else:
memory_bytes = parse_memory_string(memory_spec) # 2.5gb -> bytes
devices.append((device_name, memory_bytes))
return devices
```
Ratio Mode (llama.cpp style):
```python
# "cuda:0,25%;cpu,75%" -> 1:3 split
def parse_ratio_allocation(allocation_string):
total_ratio = sum(float(spec.split(',')[1].rstrip('%')) for spec in allocation_string.split(';'))
device_ratios = []
for device_spec in allocation_string.split(';'):
device_name, ratio_spec = device_spec.split(',')
ratio = float(ratio_spec.rstrip('%')) / total_ratio
device_ratios.append((device_name, ratio))
return device_ratios
```
### Selective Ejection Pipeline (v2.5.0 - VERIFIED WORKING)
**Load-time Flagging** (per-model transient):
```python
# In DisTorch2 wrapper after real loader returns
if hasattr(out[0], 'model') and hasattr(out[0].model, '_mgpu_keep_loaded'):
keep_loaded = out[0].model._mgpu_keep_loaded
out[0].model._mgpu_unload_distorch_model = (not keep_loaded)
```
Purpose: Mark specific DisTorch models for ejection when user unchecks "keep loaded"
**Manager-Parity Cleanup Trigger**:
```python
def force_full_system_cleanup(reason="manual", force=True):
pq.set_flag("unload_models", True) # Exactly what Manager's
pq.set_flag("free_memory", True) # "Free model and node cache" does
```
**Selective Unloading** (patched `mm.unload_all_models`):
```python
def _mgpu_patched_unload_all_models():
# Categorize models by flag
models_to_unload = [lm for lm in mm.current_loaded_models
if getattr(lm.model, '_mgpu_unload_distorch_model', False)]
kept_models = [lm for lm in mm.current_loaded_models
if not getattr(lm.model, '_mgpu_unload_distorch_model', False)]
if kept_models:
# Selective unload: eject flagged, retain others
for lm in models_to_unload:
lm.model_unload(unpatch_weights=True)
# Add GC anchors to prevent premature collection
for lm in kept_models:
add_retention_anchor(lm.model, "keep_loaded_protection")
# Rebuild with kept models only
mm.current_loaded_models = kept_models
else:
# No models to keep - standard cleanup
_mgpu_original_unload_all_models()
```
**Multi-Device VRAM + CPU Management** (patched `mm.soft_empty_cache`):
```python
def soft_empty_cache_distorch2_patched(force=False):
# 1. Detect DisTorch2 activity
is_distorch_active = any(model_hash in safetensor_allocation_store
for model in mm.current_loaded_models)
# 2. VRAM allocator management
if is_distorch_active:
soft_empty_cache_multigpu() # Clear all device caches
else:
original_soft_empty_cache(force) # Standard single-device
# 3. Adaptive CPU memory management
check_cpu_memory_threshold()
# 4. Forced executor reset (Manager parity)
if force:
trigger_executor_cache_reset(reason="forced_soft_empty", force=True)
```
**Verified Working** (Production Logs 2025-09-30):
```
[CATEGORIZE_SUMMARY] kept_models: 2, models_to_unload: 1, total: 3
[SELECTIVE_UNLOAD] Proceeding with selective unload: retaining 2, unloading 1
[UNLOAD_EXECUTE] Unloading model: Flux
[REMAINING_MODEL] 0: AutoencodingEngine
[REMAINING_MODEL] 1: FluxClipModel_
```
### Device Detection & Management
Multi-Device Enumeration:
```python
def get_device_list():
devices = ["cpu"] # Always available
if torch.cuda.is_available():
devices.extend([f"cuda:{i}" for i in range(torch.cuda.device_count())])
# XPU/NPU/MLU/MPS/DirectML/CoreX detection...
return devices
```
Device Bandwidth Intelligence (from benchmarking):
1. NVLINK (~50.8 GB/s)
2. PCIe 4.0 x16 (~27.2 GB/s)
3. PCIe 3.0 x8 (~6.8 GB/s)
4. PCIe 3.0 x4 (~2.1 GB/s)
## Integration Patterns
### ComfyCore Alignment
Philosophy: Work WITH ComfyUI, not against it
```python
# GOOD: Use ComfyCore's device management
current_device = mm.get_torch_device()
mm.text_encoder_device = target_device
# AVOID: Direct PyTorch device manipulation
torch.cuda.set_device(device_id) # Bypasses ComfyCore
```
### Node Registration System
```python
# Dynamic registration based on available dependencies
if "ComfyUI-GGUF" in installed_modules:
NODE_CLASS_MAPPINGS["UnetLoaderGGUFDisTorch2MultiGPU"] = create_gguf_distorch_node()
```
### Dependency Detection
```python
def check_module_availability(module_paths):
for path in module_paths:
if os.path.exists(os.path.join(custom_nodes_dir, path)):
return True
return False
```
## Performance Optimization Patterns
### Layer Transfer Optimization
```python
def optimized_layer_transfer(layer, source_device, target_device):
if source_device == target_device:
return layer
non_blocking = "cuda" in source_device and "cuda" in target_device
if source_device == "cpu" and "cuda" in target_device:
layer = layer.pin_memory()
return layer.to(target_device, non_blocking=non_blocking)
```
### Memory Pressure Management
```python
def should_auto_offload(model_size_gb, vram_available_gb, threshold=0.9):
return model_size_gb > (vram_available_gb * threshold)
def calculate_offload_amount(model_size_gb, target_vram_usage_gb):
return max(0, model_size_gb - target_vram_usage_gb)
```
## Error Handling Philosophy
### Fail Loudly Pattern
```python
# GOOD: Let ComfyCore changes surface immediately
def load_model(self, model_name, device):
return original_loader.load_unet(model_name, device)
# AVOID: Defensive coding that masks issues
try:
return original_loader.load_unet(model_name, device)
except AttributeError:
return fallback_method()
```
### Integration Validation
```python
def validate_comfycore_integration():
required_attrs = ['FUNCTION', 'INPUT_TYPES', 'RETURN_TYPES']
for attr in required_attrs:
if not hasattr(target_class, attr):
raise AttributeError(f"ComfyCore node missing {attr} - API changed")
```
## Code Style Patterns
### Self-Documenting Code
```python
def override_class_with_device_selection(original_class, device_param_name="device"):
compute_device = kwargs.get(device_param_name, mm.get_torch_device())
```
### Minimal Comments Philosophy
Prefer structure and naming to convey intent; use comments for non-obvious constraints/assumptions.
## Architectural Decision Records
### Why Dynamic Class Override vs Manual Definitions
Decision: Use inheritance-based class override (City96 approach)
Rationale:
- Reduces code from 400+ lines to ~50 lines
- Auto-adapts to ComfyCore changes
- Eliminates maintenance burden of manual node definitions
- Provides consistent behavior across all node types
### Why Load-Patch-Distribute vs Direct Distribution
Decision: Always load on compute device first, then distribute
Rationale:
- Ensures LoRA patches applied at full precision
- Maintains quality parity with single-GPU workflows
- Predictable behavior regardless of target device
- Works with ComfyCore’s existing patching mechanisms
### Why Expert Modes vs Automatic Only
Decision: Provide both automatic and expert allocation modes
Rationale:
- Automatic mode enables low-VRAM users immediately
- Expert modes allow optimization for specific hardware
- Performance depends on bandwidth topology; experts need control
### Why Universal Device Support vs CUDA-Only
Decision: Support CPU, XPU, NPU, MLU, MPS, DirectML alongside CUDA
Rationale:
- ComfyUI’s user base spans diverse hardware
- Future-proof for emerging accelerators
- Hardware democracy principle
### Why Per-Model Flag vs Global Sentinel (Updated)
Decision: Use per-model `_mgpu_unload_distorch_model` instead of a global “DISTORCH2_UNLOAD_MODEL” sentinel
Rationale:
- Surgical precision at model granularity
- No persistent or cross-workflow state
- Cleaner semantics under ComfyUI’s queue/flag model
Hardened unloading rule (target to re-apply):
- If no models are flagged for ejection, `mm.unload_all_models` must be a strict no-op to preserve retained models across the full Manager-parity flow.
## Testing & Validation Patterns
### Hardware Configuration Testing
```python
HARDWARE_CONFIGS = [
{"compute": "cuda:0", "donor": "cpu", "connection": "PCIe 4.0 x16"},
{"compute": "cuda:0", "donor": "cuda:1", "connection": "NVLink"},
{"compute": "cuda:0", "donor": "cuda:1", "connection": "PCIe 3.0 x8"},
{"compute": "cuda:0", "donor": "cuda:1", "connection": "PCIe 3.0 x4"},
]
```
### Model Compatibility Validation
```python
TEST_MODELS = [
{"name": "FLUX.1-dev", "format": ".safetensors", "size_gb": 23.8},
{"name": "WAN 2.2", "format": ".safetensors", "size_gb": 14.0},
{"name": "FLUX-GGUF", "format": ".gguf", "size_gb": 11.8},
{"name": "QWEN Image", "format": ".safetensors", "size_gb": 38.0},
]
```
### Performance Regression Testing
```python
def benchmark_allocation_performance(model, hardware_config, allocation_configs):
baseline_time = benchmark_single_gpu(model)
for allocation in allocation_configs:
distributed_time = benchmark_distributed(model, hardware_config, allocation)
performance_ratio = distributed_time / baseline_time
assert performance_ratio < expected_slowdown_threshold(hardware_config)
```
## Recent Refactorings (v2.5.0)
### DisTorch2 Allocation Consolidation (-179 lines)
**Problem**: 85% code duplication between `analyze_safetensor_loading()` and `analyze_safetensor_loading_clip()`
**Solution**: Unified function with CLIP support flag
```python
def _extract_clip_head_blocks(raw_block_list, compute_device):
"""Helper: Identify and pre-assign CLIP head blocks to compute device"""
head_keywords = ['embed', 'wte', 'wpe', 'token_embedding', 'position_embedding']
head_blocks = []
distributable_blocks = []
block_assignments = {}
for module_size, module_name, module_object, params in raw_block_list:
if any(kw in module_name.lower() for kw in head_keywords):
head_blocks.append((module_size, module_name, module_object, params))
block_assignments[module_name] = compute_device
else:
distributable_blocks.append((module_size, module_name, module_object, params))
return head_blocks, distributable_blocks, block_assignments, head_memory
def analyze_safetensor_loading(model_patcher, allocations_string, is_clip=False):
"""Unified allocation function with CLIP head preservation support"""
# Common allocation logic...
if is_clip:
head_blocks, distributable_raw, block_assignments, head_memory = \
_extract_clip_head_blocks(raw_block_list, compute_device)
# Adjust compute_device quota for head blocks
donor_quotas[compute_device] -= head_memory
else:
distributable_raw = raw_block_list
block_assignments = {}
# Continue with unified distribution logic...
```
**Benefits**:
- Single source of truth for allocation
- CLIP special case isolated in 20-line helper
- Easier to maintain and debug
- Same behavior, cleaner architecture
### Production Cleanup (-40 lines)
**Removed**: Diagnostic instrumentation wrapper `_mgpu_instrumented_soft_empty_cache()`
**Rationale**: Pure debug logging with no production function - removed to clean codebase
**Result**: Clear separation between device_utils.py (functional) and model_management_mgpu.py (lifecycle)
## Module Architecture (Post-Refactoring)
### Core Module Separation
Problem Solved: Eliminated circular import `device_utils.py` ↔ `distorch_2.py`
Solution: `model_management_mgpu.py` as central model lifecycle hub
### Module Responsibilities
device_utils.py (Base Layer):
- Device enumeration and detection
- VRAM cache management (`soft_empty_cache_multigpu`)
- Pure hardware abstraction – no model tracking
model_management_mgpu.py (Core Layer):
- Model lifecycle tracking and memory logging
- Cleanup orchestration (`force_full_system_cleanup`, `trigger_executor_cache_reset`, `check_cpu_memory_threshold`)
- Patched unload path (selective ejection)
distorch_2.py/distorch.py (Feature Layer):
- DisTorch distribution algorithms and allocation analysis
- Per-model flagging (`_mgpu_unload_distorch_model`) during DisTorch loads
- Imports FROM Core/Base only
UI Layer: nodes.py, checkpoint_multigpu.py
- Device-aware user interfaces and node definitions
Assembly: __init__.py
- Final integration/patch registration (`mm.soft_empty_cache` patch, node maps)
### Import Flow Architecture
```
┌─────────────────┐
│ __init__.py │ ← Assembly Layer
└─────────────────┘
↑
┌─────────────────┐
│ UI Layer │ ← nodes.py, checkpoint_multigpu.py
└─────────────────┘
↑
┌─────────────────┐
│ Feature Layer │ ← distorch_2.py, distorch.py
└─────────────────┘
↑
┌─────────────────┐
│ Core Layer │ ← model_management_mgpu.py
└─────────────────┘
↑
┌─────────────────┐
│ Base Layer │ ← device_utils.py
└─────────────────┘
```
### Architectural Validation
Rule: Dependencies only flow UPWARD. Violations create circular imports.
Prevention: Before any import, verify it respects the layer hierarchy.
### Function Migration Record
Moved from device_utils.py to model_management_mgpu.py:
- `multigpu_memory_log` – memory state logging
- `trigger_executor_cache_reset` – CPU memory management
- `check_cpu_memory_threshold` – adaptive cleanup triggers
- `force_full_system_cleanup` – Manager-parity free flow
Rationale: These belong to model lifecycle/cleanup, not hardware enumeration.