Skip to content

Commit e9a294f

Browse files
authored
Merge branch 'main' into fix/inpaint_gen
2 parents b213335 + 8323359 commit e9a294f

21 files changed

Lines changed: 1043 additions & 49 deletions

File tree

docs/installation/INSTALLATION.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ This method is recommended for experienced users and developers
2525
#### [Docker Installation](040_INSTALL_DOCKER.md)
2626
This method is recommended for those familiar with running Docker containers
2727
### Other Installation Guides
28-
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
29-
- [XFormers](installation/070_INSTALL_XFORMERS.md)
30-
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
31-
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
28+
- [PyPatchMatch](060_INSTALL_PATCHMATCH.md)
29+
- [XFormers](070_INSTALL_XFORMERS.md)
30+
- [CUDA and ROCm Drivers](030_INSTALL_CUDA_AND_ROCM.md)
31+
- [Installing New Models](050_INSTALLING_MODELS.md)
3232

3333
## :fontawesome-solid-computer: Hardware Requirements
3434

invokeai/app/services/invocation_stats.py

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
writes to the system log is stored in InvocationServices.performance_statistics.
3030
"""
3131

32+
import psutil
3233
import time
3334
from abc import ABC, abstractmethod
3435
from contextlib import AbstractContextManager
@@ -42,6 +43,11 @@
4243
from ..invocations.baseinvocation import BaseInvocation
4344
from .graph import GraphExecutionState
4445
from .item_storage import ItemStorageABC
46+
from .model_manager_service import ModelManagerService
47+
from invokeai.backend.model_management.model_cache import CacheStats
48+
49+
# size of GIG in bytes
50+
GIG = 1073741824
4551

4652

4753
class InvocationStatsServiceBase(ABC):
@@ -89,6 +95,8 @@ def update_invocation_stats(
8995
invocation_type: str,
9096
time_used: float,
9197
vram_used: float,
98+
ram_used: float,
99+
ram_changed: float,
92100
):
93101
"""
94102
Add timing information on execution of a node. Usually
@@ -97,6 +105,8 @@ def update_invocation_stats(
97105
:param invocation_type: String literal type of the node
98106
:param time_used: Time used by node's exection (sec)
99107
:param vram_used: Maximum VRAM used during exection (GB)
108+
:param ram_used: Current RAM available (GB)
109+
:param ram_changed: Change in RAM usage over course of the run (GB)
100110
"""
101111
pass
102112

@@ -115,6 +125,9 @@ class NodeStats:
115125
calls: int = 0
116126
time_used: float = 0.0 # seconds
117127
max_vram: float = 0.0 # GB
128+
cache_hits: int = 0
129+
cache_misses: int = 0
130+
cache_high_watermark: int = 0
118131

119132

120133
@dataclass
@@ -133,31 +146,62 @@ def __init__(self, graph_execution_manager: ItemStorageABC["GraphExecutionState"
133146
self.graph_execution_manager = graph_execution_manager
134147
# {graph_id => NodeLog}
135148
self._stats: Dict[str, NodeLog] = {}
149+
self._cache_stats: Dict[str, CacheStats] = {}
150+
self.ram_used: float = 0.0
151+
self.ram_changed: float = 0.0
136152

137153
class StatsContext:
138-
def __init__(self, invocation: BaseInvocation, graph_id: str, collector: "InvocationStatsServiceBase"):
154+
"""Context manager for collecting statistics."""
155+
156+
invocation: BaseInvocation = None
157+
collector: "InvocationStatsServiceBase" = None
158+
graph_id: str = None
159+
start_time: int = 0
160+
ram_used: int = 0
161+
model_manager: ModelManagerService = None
162+
163+
def __init__(
164+
self,
165+
invocation: BaseInvocation,
166+
graph_id: str,
167+
model_manager: ModelManagerService,
168+
collector: "InvocationStatsServiceBase",
169+
):
170+
"""Initialize statistics for this run."""
139171
self.invocation = invocation
140172
self.collector = collector
141173
self.graph_id = graph_id
142174
self.start_time = 0
175+
self.ram_used = 0
176+
self.model_manager = model_manager
143177

144178
def __enter__(self):
145179
self.start_time = time.time()
146180
if torch.cuda.is_available():
147181
torch.cuda.reset_peak_memory_stats()
182+
self.ram_used = psutil.Process().memory_info().rss
183+
if self.model_manager:
184+
self.model_manager.collect_cache_stats(self.collector._cache_stats[self.graph_id])
148185

149186
def __exit__(self, *args):
187+
"""Called on exit from the context."""
188+
ram_used = psutil.Process().memory_info().rss
189+
self.collector.update_mem_stats(
190+
ram_used=ram_used / GIG,
191+
ram_changed=(ram_used - self.ram_used) / GIG,
192+
)
150193
self.collector.update_invocation_stats(
151-
self.graph_id,
152-
self.invocation.type,
153-
time.time() - self.start_time,
154-
torch.cuda.max_memory_allocated() / 1e9 if torch.cuda.is_available() else 0.0,
194+
graph_id=self.graph_id,
195+
invocation_type=self.invocation.type,
196+
time_used=time.time() - self.start_time,
197+
vram_used=torch.cuda.max_memory_allocated() / GIG if torch.cuda.is_available() else 0.0,
155198
)
156199

157200
def collect_stats(
158201
self,
159202
invocation: BaseInvocation,
160203
graph_execution_state_id: str,
204+
model_manager: ModelManagerService,
161205
) -> StatsContext:
162206
"""
163207
Return a context object that will capture the statistics.
@@ -166,7 +210,8 @@ def collect_stats(
166210
"""
167211
if not self._stats.get(graph_execution_state_id): # first time we're seeing this
168212
self._stats[graph_execution_state_id] = NodeLog()
169-
return self.StatsContext(invocation, graph_execution_state_id, self)
213+
self._cache_stats[graph_execution_state_id] = CacheStats()
214+
return self.StatsContext(invocation, graph_execution_state_id, model_manager, self)
170215

171216
def reset_all_stats(self):
172217
"""Zero all statistics"""
@@ -179,13 +224,36 @@ def reset_stats(self, graph_execution_id: str):
179224
except KeyError:
180225
logger.warning(f"Attempted to clear statistics for unknown graph {graph_execution_id}")
181226

182-
def update_invocation_stats(self, graph_id: str, invocation_type: str, time_used: float, vram_used: float):
227+
def update_mem_stats(
228+
self,
229+
ram_used: float,
230+
ram_changed: float,
231+
):
232+
"""
233+
Update the collector with RAM memory usage info.
234+
235+
:param ram_used: How much RAM is currently in use.
236+
:param ram_changed: How much RAM changed since last generation.
237+
"""
238+
self.ram_used = ram_used
239+
self.ram_changed = ram_changed
240+
241+
def update_invocation_stats(
242+
self,
243+
graph_id: str,
244+
invocation_type: str,
245+
time_used: float,
246+
vram_used: float,
247+
):
183248
"""
184249
Add timing information on execution of a node. Usually
185250
used internally.
186251
:param graph_id: ID of the graph that is currently executing
187252
:param invocation_type: String literal type of the node
188-
:param time_used: Floating point seconds used by node's exection
253+
:param time_used: Time used by node's exection (sec)
254+
:param vram_used: Maximum VRAM used during exection (GB)
255+
:param ram_used: Current RAM available (GB)
256+
:param ram_changed: Change in RAM usage over course of the run (GB)
189257
"""
190258
if not self._stats[graph_id].nodes.get(invocation_type):
191259
self._stats[graph_id].nodes[invocation_type] = NodeStats()
@@ -197,7 +265,7 @@ def update_invocation_stats(self, graph_id: str, invocation_type: str, time_used
197265
def log_stats(self):
198266
"""
199267
Send the statistics to the system logger at the info level.
200-
Stats will only be printed if when the execution of the graph
268+
Stats will only be printed when the execution of the graph
201269
is complete.
202270
"""
203271
completed = set()
@@ -208,16 +276,30 @@ def log_stats(self):
208276

209277
total_time = 0
210278
logger.info(f"Graph stats: {graph_id}")
211-
logger.info("Node Calls Seconds VRAM Used")
279+
logger.info(f"{'Node':>30} {'Calls':>7}{'Seconds':>9} {'VRAM Used':>10}")
212280
for node_type, stats in self._stats[graph_id].nodes.items():
213-
logger.info(f"{node_type:<20} {stats.calls:>5} {stats.time_used:7.3f}s {stats.max_vram:4.2f}G")
281+
logger.info(f"{node_type:>30} {stats.calls:>4} {stats.time_used:7.3f}s {stats.max_vram:4.3f}G")
214282
total_time += stats.time_used
215283

284+
cache_stats = self._cache_stats[graph_id]
285+
hwm = cache_stats.high_watermark / GIG
286+
tot = cache_stats.cache_size / GIG
287+
loaded = sum([v for v in cache_stats.loaded_model_sizes.values()]) / GIG
288+
216289
logger.info(f"TOTAL GRAPH EXECUTION TIME: {total_time:7.3f}s")
290+
logger.info("RAM used by InvokeAI process: " + "%4.2fG" % self.ram_used + f" ({self.ram_changed:+5.3f}G)")
291+
logger.info(f"RAM used to load models: {loaded:4.2f}G")
217292
if torch.cuda.is_available():
218-
logger.info("Current VRAM utilization " + "%4.2fG" % (torch.cuda.memory_allocated() / 1e9))
293+
logger.info("VRAM in use: " + "%4.3fG" % (torch.cuda.memory_allocated() / GIG))
294+
logger.info("RAM cache statistics:")
295+
logger.info(f" Model cache hits: {cache_stats.hits}")
296+
logger.info(f" Model cache misses: {cache_stats.misses}")
297+
logger.info(f" Models cached: {cache_stats.in_cache}")
298+
logger.info(f" Models cleared from cache: {cache_stats.cleared}")
299+
logger.info(f" Cache high water mark: {hwm:4.2f}/{tot:4.2f}G")
219300

220301
completed.add(graph_id)
221302

222303
for graph_id in completed:
223304
del self._stats[graph_id]
305+
del self._cache_stats[graph_id]

invokeai/app/services/model_manager_service.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ModelNotFoundException,
2323
)
2424
from invokeai.backend.model_management.model_search import FindModels
25+
from invokeai.backend.model_management.model_cache import CacheStats
2526

2627
import torch
2728
from invokeai.app.models.exceptions import CanceledException
@@ -276,6 +277,13 @@ def sync_to_config(self):
276277
"""
277278
pass
278279

280+
@abstractmethod
281+
def collect_cache_stats(self, cache_stats: CacheStats):
282+
"""
283+
Reset model cache statistics for graph with graph_id.
284+
"""
285+
pass
286+
279287
@abstractmethod
280288
def commit(self, conf_file: Optional[Path] = None) -> None:
281289
"""
@@ -500,6 +508,12 @@ def convert_model(
500508
self.logger.debug(f"convert model {model_name}")
501509
return self.mgr.convert_model(model_name, base_model, model_type, convert_dest_directory)
502510

511+
def collect_cache_stats(self, cache_stats: CacheStats):
512+
"""
513+
Reset model cache statistics for graph with graph_id.
514+
"""
515+
self.mgr.cache.stats = cache_stats
516+
503517
def commit(self, conf_file: Optional[Path] = None):
504518
"""
505519
Write current configuration out to the indicated file.

invokeai/app/services/processor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def __process(self, stop_event: Event):
8686

8787
# Invoke
8888
try:
89-
with statistics.collect_stats(invocation, graph_execution_state.id):
89+
graph_id = graph_execution_state.id
90+
model_manager = self.__invoker.services.model_manager
91+
with statistics.collect_stats(invocation, graph_id, model_manager):
9092
# use the internal invoke_internal(), which wraps the node's invoke() method in
9193
# this accomodates nodes which require a value, but get it only from a
9294
# connection

invokeai/backend/model_management/model_cache.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
import sys
2222
import hashlib
2323
from contextlib import suppress
24+
from dataclasses import dataclass, field
2425
from pathlib import Path
2526
from typing import Dict, Union, types, Optional, Type, Any
2627

2728
import torch
2829

29-
import logging
3030
import invokeai.backend.util.logging as logger
3131
from .models import BaseModelType, ModelType, SubModelType, ModelBase
3232

@@ -41,6 +41,18 @@
4141
GIG = 1073741824
4242

4343

44+
@dataclass
45+
class CacheStats(object):
46+
hits: int = 0 # cache hits
47+
misses: int = 0 # cache misses
48+
high_watermark: int = 0 # amount of cache used
49+
in_cache: int = 0 # number of models in cache
50+
cleared: int = 0 # number of models cleared to make space
51+
cache_size: int = 0 # total size of cache
52+
# {submodel_key => size}
53+
loaded_model_sizes: Dict[str, int] = field(default_factory=dict)
54+
55+
4456
class ModelLocker(object):
4557
"Forward declaration"
4658
pass
@@ -115,6 +127,9 @@ def __init__(
115127
self.sha_chunksize = sha_chunksize
116128
self.logger = logger
117129

130+
# used for stats collection
131+
self.stats = None
132+
118133
self._cached_models = dict()
119134
self._cache_stack = list()
120135

@@ -181,13 +196,14 @@ def get_model(
181196
model_type=model_type,
182197
submodel_type=submodel,
183198
)
184-
185199
# TODO: lock for no copies on simultaneous calls?
186200
cache_entry = self._cached_models.get(key, None)
187201
if cache_entry is None:
188202
self.logger.info(
189203
f"Loading model {model_path}, type {base_model.value}:{model_type.value}{':'+submodel.value if submodel else ''}"
190204
)
205+
if self.stats:
206+
self.stats.misses += 1
191207

192208
# this will remove older cached models until
193209
# there is sufficient room to load the requested model
@@ -201,6 +217,17 @@ def get_model(
201217

202218
cache_entry = _CacheRecord(self, model, mem_used)
203219
self._cached_models[key] = cache_entry
220+
else:
221+
if self.stats:
222+
self.stats.hits += 1
223+
224+
if self.stats:
225+
self.stats.cache_size = self.max_cache_size * GIG
226+
self.stats.high_watermark = max(self.stats.high_watermark, self._cache_size())
227+
self.stats.in_cache = len(self._cached_models)
228+
self.stats.loaded_model_sizes[key] = max(
229+
self.stats.loaded_model_sizes.get(key, 0), model_info.get_size(submodel)
230+
)
204231

205232
with suppress(Exception):
206233
self._cache_stack.remove(key)
@@ -280,14 +307,14 @@ def model_hash(
280307
"""
281308
Given the HF repo id or path to a model on disk, returns a unique
282309
hash. Works for legacy checkpoint files, HF models on disk, and HF repo IDs
310+
283311
:param model_path: Path to model file/directory on disk.
284312
"""
285313
return self._local_model_hash(model_path)
286314

287315
def cache_size(self) -> float:
288-
"Return the current size of the cache, in GB"
289-
current_cache_size = sum([m.size for m in self._cached_models.values()])
290-
return current_cache_size / GIG
316+
"""Return the current size of the cache, in GB."""
317+
return self._cache_size() / GIG
291318

292319
def _has_cuda(self) -> bool:
293320
return self.execution_device.type == "cuda"
@@ -310,12 +337,15 @@ def _print_cuda_stats(self):
310337
f"Current VRAM/RAM usage: {vram}/{ram}; cached_models/loaded_models/locked_models/ = {cached_models}/{loaded_models}/{locked_models}"
311338
)
312339

340+
def _cache_size(self) -> int:
341+
return sum([m.size for m in self._cached_models.values()])
342+
313343
def _make_cache_room(self, model_size):
314344
# calculate how much memory this model will require
315345
# multiplier = 2 if self.precision==torch.float32 else 1
316346
bytes_needed = model_size
317347
maximum_size = self.max_cache_size * GIG # stored in GB, convert to bytes
318-
current_size = sum([m.size for m in self._cached_models.values()])
348+
current_size = self._cache_size()
319349

320350
if current_size + bytes_needed > maximum_size:
321351
self.logger.debug(
@@ -364,6 +394,8 @@ def _make_cache_room(self, model_size):
364394
f"Unloading model {model_key} to free {(model_size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)"
365395
)
366396
current_size -= cache_entry.size
397+
if self.stats:
398+
self.stats.cleared += 1
367399
del self._cache_stack[pos]
368400
del self._cached_models[model_key]
369401
del cache_entry

invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def do_controlnet_step(
240240
controlnet_cond=control_datum.image_tensor,
241241
conditioning_scale=controlnet_weight, # controlnet specific, NOT the guidance scale
242242
encoder_attention_mask=encoder_attention_mask,
243+
added_cond_kwargs=added_cond_kwargs,
243244
guess_mode=soft_injection, # this is still called guess_mode in diffusers ControlNetModel
244245
return_dict=False,
245246
)

0 commit comments

Comments
 (0)