Skip to content

Commit ac69219

Browse files
committed
fix: vram not release after indexing
1 parent 0d97bdc commit ac69219

6 files changed

Lines changed: 97 additions & 3 deletions

File tree

src/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ def index_codebase(
334334

335335
metadata_index.mark_reindex_complete()
336336

337+
# Clear VRAM cache after heavy indexing
338+
self.embedding_model.clear_cache()
339+
337340
return total_chunks
338341

339342
def incremental_reindex(
@@ -459,6 +462,9 @@ def incremental_reindex(
459462
# Mark reindex complete
460463
metadata_index.mark_reindex_complete()
461464

465+
# Clear VRAM cache after heavy indexing
466+
self.embedding_model.clear_cache()
467+
462468
return {
463469
'added_count': len(added),
464470
'modified_count': len(modified),

src/embedding_server.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,23 @@ def _load_models(self):
129129
self._embedding_model = OpenAIEmbedding(model_name)
130130
else:
131131
from .embeddings.sentence_transformer_embedding import SentenceTransformerEmbedding
132-
# No idle timeout - server manages lifecycle
133-
self._embedding_model = SentenceTransformerEmbedding(model_name, lazy_load=False, idle_timeout=0)
132+
# Use configured idle timeout (default 30 min) to release VRAM
133+
idle_timeout = self.config.get_model_idle_timeout()
134+
self._embedding_model = SentenceTransformerEmbedding(
135+
model_name,
136+
lazy_load=False,
137+
idle_timeout=idle_timeout
138+
)
134139

135140
# Load reranker if enabled
136141
if self.config.is_reranker_enabled():
137142
print(f"Loading reranker model: {self.config.get_reranker_model()}", file=sys.stderr)
138143
from .reranker.cross_encoder_reranker import CrossEncoderReranker
144+
idle_timeout = self.config.get_model_idle_timeout()
139145
self._reranker = CrossEncoderReranker(
140146
self.config.get_reranker_model(),
141147
lazy_load=False,
142-
idle_timeout=0
148+
idle_timeout=idle_timeout
143149
)
144150

145151
self._models_loaded = True
@@ -242,6 +248,23 @@ async def embedding_dimension(request: DimensionRequest):
242248
dim = self._embedding_model.get_embedding_dimension()
243249
return {"dimension": dim}
244250

251+
@app.post("/clear_cache")
252+
async def clear_cache(request: DimensionRequest):
253+
"""Clear memory cache (CUDA) without unloading models."""
254+
self._update_heartbeat(request.client_id)
255+
256+
with self._models_lock:
257+
if self._embedding_model:
258+
if hasattr(self._embedding_model, "clear_cache"):
259+
self._embedding_model.clear_cache()
260+
261+
# Also clear reranker cache if loaded
262+
if self._reranker:
263+
if hasattr(self._reranker, "clear_cache"):
264+
self._reranker.clear_cache()
265+
266+
return {"status": "ok"}
267+
245268
@app.post("/rerank")
246269
async def rerank(request: RerankRequest):
247270
"""Rerank documents."""

src/embeddings/embedding_interface.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,20 @@ def embed_batch(self, texts: List[str]) -> List[List[float]]:
4747
A list of embedding vectors
4848
"""
4949
pass
50+
51+
@abstractmethod
52+
def get_embedding_dimension(self) -> int:
53+
"""
54+
Get the dimension of the embedding vectors.
55+
56+
Returns:
57+
The dimension of embeddings produced by this model
58+
"""
59+
pass
60+
61+
def clear_cache(self):
62+
"""
63+
Clear memory cache (e.g. CUDA memory) without unloading the model.
64+
Useful after memory-intensive operations like indexing.
65+
"""
66+
pass

src/embeddings/http_embedding.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,15 @@ def start_background_loading(self):
258258
def unload_model(self):
259259
"""No-op for HTTP client - server manages model lifecycle."""
260260
pass
261+
262+
def clear_cache(self):
263+
"""Request server to clear memory cache."""
264+
try:
265+
# We don't retry this if it fails, as it's an optimization only
266+
requests.post(
267+
f"{self.base_url}/clear_cache",
268+
json={"client_id": self.client_id},
269+
timeout=CONNECTION_TIMEOUT
270+
)
271+
except (requests.ConnectionError, requests.Timeout):
272+
pass

src/embeddings/sentence_transformer_embedding.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ def _cleanup_loop(self):
9898
self.model = None
9999
# Force garbage collection to release GPU memory if applicable
100100
gc.collect()
101+
try:
102+
import torch
103+
if torch.cuda.is_available():
104+
torch.cuda.empty_cache()
105+
except ImportError:
106+
pass
101107

102108
def _ensure_model_loaded(self):
103109
"""Ensure the model is loaded before use (blocks if still loading)."""
@@ -174,6 +180,12 @@ def unload_model(self):
174180
del self.model
175181
self.model = None
176182
gc.collect()
183+
try:
184+
import torch
185+
if torch.cuda.is_available():
186+
torch.cuda.empty_cache()
187+
except ImportError:
188+
pass
177189

178190
def stop_cleanup_thread(self):
179191
"""Stop the background cleanup thread."""
@@ -185,3 +197,15 @@ def __del__(self):
185197
"""Cleanup when the object is destroyed."""
186198
self.stop_cleanup_thread()
187199
self.unload_model()
200+
201+
def clear_cache(self):
202+
"""Clear memory cache (CUDA memory) without unloading the model."""
203+
with self._loading_lock:
204+
# Force garbage collection
205+
gc.collect()
206+
try:
207+
import torch
208+
if torch.cuda.is_available():
209+
torch.cuda.empty_cache()
210+
except ImportError:
211+
pass

src/reranker/cross_encoder_reranker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ def _cleanup_loop(self):
8585
self.model = None
8686
# Force garbage collection to release GPU memory if applicable
8787
gc.collect()
88+
try:
89+
import torch
90+
if torch.cuda.is_available():
91+
torch.cuda.empty_cache()
92+
except ImportError:
93+
pass
8894

8995
def _ensure_model_loaded(self):
9096
"""Ensure the model is loaded before use (blocks if still loading)."""
@@ -141,6 +147,12 @@ def unload_model(self):
141147
del self.model
142148
self.model = None
143149
gc.collect()
150+
try:
151+
import torch
152+
if torch.cuda.is_available():
153+
torch.cuda.empty_cache()
154+
except ImportError:
155+
pass
144156

145157
def stop_cleanup_thread(self):
146158
"""Stop the background cleanup thread."""

0 commit comments

Comments
 (0)