# 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 (Current) Updated to reflect current code (Phase 3 implemented without global sentinel): - Load-time flagging (per-model transient): - In each DisTorch2 override, after the real loader returns: - `out[0].model._mgpu_unload_distorch_model = (keep_loaded == False)` - Purpose: mark this specific DisTorch model for ejection only when the user unchecked “keep loaded”. - Manager-parity cleanup trigger: - `force_full_system_cleanup(reason, force=True)` sets: - `unload_models=True`, `free_memory=True` on PromptQueue (exactly what Manager’s “Free model and node cache” does). - Selective unloading: - `mm.unload_all_models` is patched (`_mgpu_patched_unload_all_models` in `model_management_mgpu.py`): - Splits `mm.current_loaded_models` into `models_to_unload` (flag==True) and `kept_models` (flag==False). - If any are flagged, unloads only those and resets `mm.current_loaded_models = kept_models`. - Note: If no models are flagged, the current code delegates to the original `unload_all_models()` (this is under review; see “Hardened Rule” below). - Multi-device VRAM cache + CPU reset: - `mm.soft_empty_cache` is patched to `soft_empty_cache_distorch2_patched`: - Detects DisTorch2-active state and clears allocator caches on all devices via `soft_empty_cache_multigpu()` - Adaptive CPU memory reset (threshold-based), and optional forced `PromptExecutor.reset()` when `force=True` for Manager parity. Hardened Rule (target behavior to restore): - If `models_to_unload` is empty, `unload_all_models` should be a strict no-op (do not delegate to the original). Retained models must never be ejected when no flags are set. This will be re-applied during the rediscovery step. ### 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) ``` ## 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.