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.
467 lines
16 KiB
Markdown
467 lines
16 KiB
Markdown
# 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.
|