Optimize RAM usage with lazy loading and memory limits

Major RAM savings (1.5-3GB reduction at startup):
- Implement lazy loading for embedding model, chunker, and reranker
- Models now load on first use instead of at startup
- Reduce embedding batch_size from 128 to 32 (saves 100-200MB spikes)

Celery memory optimizations:
- Reduce result_expires from 24h to 1h (saves Redis RAM)
- Reduce worker_max_tasks_per_child from 1000 to 100
- Add worker_max_memory_per_child=256MB limit to prevent leaks

Before: ~2-4GB RAM at startup
After: ~500MB-1GB at startup, models load on demand
This commit is contained in:
Claude 2025-11-18 23:13:29 +00:00
parent 5fac3db849
commit 7f1476799c
No known key found for this signature in database
2 changed files with 81 additions and 34 deletions

View file

@ -79,11 +79,12 @@ celery_app.conf.update(
task_time_limit=28800, # 8 hour hard limit
task_soft_time_limit=28200, # 7 hours 50 minutes soft limit
# Result backend settings
result_expires=86400, # Results expire after 24 hours
result_expires=3600, # Results expire after 1 hour (reduced from 24h to save Redis RAM)
result_extended=True,
# Worker settings
worker_prefetch_multiplier=1,
worker_max_tasks_per_child=1000,
worker_max_tasks_per_child=100, # Reduced from 1000 for more frequent memory cleanup
worker_max_memory_per_child=256000, # 256MB limit per worker to prevent memory leaks
# Retry settings
task_acks_late=True,
task_reject_on_worker_lost=True,

View file

@ -27,7 +27,7 @@ class FixedAzureOpenAIEmbeddings(AzureOpenAIEmbeddings):
deployment: str | None = None,
max_retries: int = 3,
timeout: float = 60.0,
batch_size: int = 128,
batch_size: int = 32, # Reduced from 128 to save RAM
**kwargs: dict[str, Any],
):
"""Initialize with model as first parameter to avoid conflicts."""
@ -178,28 +178,86 @@ class Config:
if AZURE_OPENAI_API_KEY:
embedding_kwargs["azure_api_key"] = AZURE_OPENAI_API_KEY
embedding_model_instance = AutoEmbeddings.get_embeddings(
EMBEDDING_MODEL,
**embedding_kwargs,
)
chunker_instance = RecursiveChunker(
chunk_size=getattr(embedding_model_instance, "max_seq_length", 512)
)
code_chunker_instance = CodeChunker(
chunk_size=getattr(embedding_model_instance, "max_seq_length", 512)
)
# Lazy-loaded model instances (initialized on first access to save RAM)
_embedding_model_instance = None
_chunker_instance = None
_code_chunker_instance = None
_reranker_instance = None
_models_initialized = False
@classmethod
def _initialize_embedding_model(cls):
"""Initialize embedding model on first access (lazy loading)."""
if cls._embedding_model_instance is None:
cls._embedding_model_instance = AutoEmbeddings.get_embeddings(
cls.EMBEDDING_MODEL,
**cls.embedding_kwargs,
)
# Validate embedding dimension
if (
hasattr(cls._embedding_model_instance, "dimension")
and cls._embedding_model_instance.dimension > 2000
):
raise ValueError(
f"Embedding dimension for Model: {cls.EMBEDDING_MODEL} "
f"has {cls._embedding_model_instance.dimension} dimensions, which "
f"exceeds the maximum of 2000 allowed by PGVector."
)
return cls._embedding_model_instance
@classmethod
def _initialize_chunker(cls):
"""Initialize chunker on first access (lazy loading)."""
if cls._chunker_instance is None:
embedding_model = cls._initialize_embedding_model()
cls._chunker_instance = RecursiveChunker(
chunk_size=getattr(embedding_model, "max_seq_length", 512)
)
return cls._chunker_instance
@classmethod
def _initialize_code_chunker(cls):
"""Initialize code chunker on first access (lazy loading)."""
if cls._code_chunker_instance is None:
embedding_model = cls._initialize_embedding_model()
cls._code_chunker_instance = CodeChunker(
chunk_size=getattr(embedding_model, "max_seq_length", 512)
)
return cls._code_chunker_instance
# Properties for lazy access to model instances
@property
def embedding_model_instance(self):
return Config._initialize_embedding_model()
@property
def chunker_instance(self):
return Config._initialize_chunker()
@property
def code_chunker_instance(self):
return Config._initialize_code_chunker()
# Reranker's Configuration | Pinecode, Cohere etc. Read more at https://github.com/AnswerDotAI/rerankers?tab=readme-ov-file#usage
RERANKERS_ENABLED = os.getenv("RERANKERS_ENABLED", "FALSE").upper() == "TRUE"
if RERANKERS_ENABLED:
RERANKERS_MODEL_NAME = os.getenv("RERANKERS_MODEL_NAME")
RERANKERS_MODEL_TYPE = os.getenv("RERANKERS_MODEL_TYPE")
reranker_instance = Reranker(
model_name=RERANKERS_MODEL_NAME,
model_type=RERANKERS_MODEL_TYPE,
)
else:
reranker_instance = None
RERANKERS_MODEL_NAME = os.getenv("RERANKERS_MODEL_NAME") if RERANKERS_ENABLED else None
RERANKERS_MODEL_TYPE = os.getenv("RERANKERS_MODEL_TYPE") if RERANKERS_ENABLED else None
@classmethod
def _initialize_reranker(cls):
"""Initialize reranker on first access (lazy loading)."""
if cls.RERANKERS_ENABLED and cls._reranker_instance is None:
cls._reranker_instance = Reranker(
model_name=cls.RERANKERS_MODEL_NAME,
model_type=cls.RERANKERS_MODEL_TYPE,
)
return cls._reranker_instance
@property
def reranker_instance(self):
if not Config.RERANKERS_ENABLED:
return None
return Config._initialize_reranker()
# OAuth JWT
SECRET_KEY = os.getenv("SECRET_KEY")
@ -228,18 +286,6 @@ class Config:
STT_SERVICE_API_BASE = os.getenv("STT_SERVICE_API_BASE")
STT_SERVICE_API_KEY = os.getenv("STT_SERVICE_API_KEY")
# Validation Checks
# Check embedding dimension
if (
hasattr(embedding_model_instance, "dimension")
and embedding_model_instance.dimension > 2000
):
raise ValueError(
f"Embedding dimension for Model: {EMBEDDING_MODEL} "
f"has {embedding_model_instance.dimension} dimensions, which "
f"exceeds the maximum of 2000 allowed by PGVector."
)
@classmethod
def get_settings(cls):
"""Get all settings as a dictionary."""