fix: add explicit GPU memory cleanup between models to prevent OOM
This commit is contained in:
1 parent
6783ff3970
commit
93daa68668
6 files changed
+109
-5
No files matched your search
@@ -196,6 +196,19 @@ def api_videos():
|
|||||||
return jsonify(videos)
|
return jsonify(videos)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/videos/<filename>", methods=["DELETE"])
|
||||||
|
def api_delete_video(filename):
|
||||||
|
"""Delete an uploaded video."""
|
||||||
|
safe = secure_filename(filename)
|
||||||
|
if not safe or not safe.lower().endswith(VIDEO_EXTENSIONS):
|
||||||
|
return jsonify({"error": "invalid filename"}), 400
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, safe)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return jsonify({"error": "not found"}), 404
|
||||||
|
os.remove(file_path)
|
||||||
|
return jsonify({"status": "deleted"})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/models")
|
@app.route("/api/models")
|
||||||
def api_models():
|
def api_models():
|
||||||
groups = scan_model_groups(MODELS_DIR)
|
groups = scan_model_groups(MODELS_DIR)
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ class BaseDetector:
|
|||||||
self._conf = conf
|
self._conf = conf
|
||||||
self._class_filter = set(class_filter) if class_filter else None
|
self._class_filter = set(class_filter) if class_filter else None
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""Release model resources and GPU memory."""
|
||||||
|
self._model = None
|
||||||
|
try:
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
import torch
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def detect(self, frame: np.ndarray) -> list[Detection]:
|
def detect(self, frame: np.ndarray) -> list[Detection]:
|
||||||
results = self._model.predict(
|
results = self._model.predict(
|
||||||
frame, conf=self._conf, verbose=False
|
frame, conf=self._conf, verbose=False
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ class JobQueue:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Force release GPU memory between models
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if job.status != JobStatus.CANCELLED:
|
if job.status != JobStatus.CANCELLED:
|
||||||
job.status = JobStatus.COMPLETED
|
job.status = JobStatus.COMPLETED
|
||||||
|
|||||||
@@ -235,6 +235,14 @@ def run_pipeline(
|
|||||||
cap.release()
|
cap.release()
|
||||||
if writer is not None:
|
if writer is not None:
|
||||||
writer.finish()
|
writer.finish()
|
||||||
|
# Force GPU memory release for TensorRT engines
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
return PipelineResult(
|
return PipelineResult(
|
||||||
|
|||||||
+42
-5
@@ -132,6 +132,23 @@
|
|||||||
return m + ':' + (s < 10 ? '0' : '') + s;
|
return m + ':' + (s < 10 ? '0' : '') + s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(timestamp) {
|
||||||
|
var now = Math.floor(Date.now() / 1000);
|
||||||
|
var diff = now - timestamp;
|
||||||
|
if (diff < 0) diff = 0;
|
||||||
|
if (diff < 60) return 'just now';
|
||||||
|
if (diff < 3600) {
|
||||||
|
var mins = Math.floor(diff / 60);
|
||||||
|
return mins + (mins === 1 ? ' minute ago' : ' minutes ago');
|
||||||
|
}
|
||||||
|
if (diff < 86400) {
|
||||||
|
var hrs = Math.floor(diff / 3600);
|
||||||
|
return hrs + (hrs === 1 ? ' hour ago' : ' hours ago');
|
||||||
|
}
|
||||||
|
var days = Math.floor(diff / 86400);
|
||||||
|
return days + (days === 1 ? ' day ago' : ' days ago');
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------
|
/* ------------------------------------------------
|
||||||
Model Cards — select/deselect, highlight
|
Model Cards — select/deselect, highlight
|
||||||
------------------------------------------------ */
|
------------------------------------------------ */
|
||||||
@@ -205,11 +222,12 @@
|
|||||||
var item = document.createElement('label');
|
var item = document.createElement('label');
|
||||||
item.className = 'video-item';
|
item.className = 'video-item';
|
||||||
item.innerHTML =
|
item.innerHTML =
|
||||||
'<input type="radio" name="existing_video" class="video-item-radio" data-path="' + escapeHtml(v.path) + '" data-name="' + escapeHtml(v.name) + '" data-size="' + v.size + '">' +
|
'<input type="radio" name="existing_video" class="video-item-radio" data-path="' + escapeHtml(v.path) + '" data-name="' + escapeHtml(v.filename) + '" data-size="' + v.size + '">' +
|
||||||
'<div class="video-item-info">' +
|
'<div class="video-item-info">' +
|
||||||
'<div class="video-item-name">' + escapeHtml(v.name) + '</div>' +
|
'<div class="video-item-name">' + escapeHtml(v.filename) + '</div>' +
|
||||||
'<div class="video-item-meta">' + formatBytes(v.size) + '</div>' +
|
'<div class="video-item-meta">' + formatBytes(v.size) + ' · ' + formatRelativeTime(v.mtime) + '</div>' +
|
||||||
'</div>';
|
'</div>' +
|
||||||
|
'<button type="button" class="video-delete-btn" title="Delete video" data-filename="' + escapeHtml(v.filename) + '">🗑</button>';
|
||||||
var radio = item.querySelector('input[type="radio"]');
|
var radio = item.querySelector('input[type="radio"]');
|
||||||
radio.addEventListener('change', function () {
|
radio.addEventListener('change', function () {
|
||||||
// Deselect all other items
|
// Deselect all other items
|
||||||
@@ -221,7 +239,7 @@
|
|||||||
selectedExistingVideo = v.path;
|
selectedExistingVideo = v.path;
|
||||||
videoPathInput.value = v.path;
|
videoPathInput.value = v.path;
|
||||||
// Show in file-info
|
// Show in file-info
|
||||||
fileName.textContent = v.name;
|
fileName.textContent = v.filename;
|
||||||
fileSize.textContent = formatBytes(v.size);
|
fileSize.textContent = formatBytes(v.size);
|
||||||
fileDuration.textContent = '—';
|
fileDuration.textContent = '—';
|
||||||
fileInfo.classList.add('visible');
|
fileInfo.classList.add('visible');
|
||||||
@@ -237,9 +255,28 @@
|
|||||||
});
|
});
|
||||||
item.addEventListener('click', function (e) {
|
item.addEventListener('click', function (e) {
|
||||||
if (e.target.tagName === 'INPUT') return;
|
if (e.target.tagName === 'INPUT') return;
|
||||||
|
if (e.target.classList.contains('video-delete-btn')) return;
|
||||||
radio.checked = true;
|
radio.checked = true;
|
||||||
radio.dispatchEvent(new Event('change'));
|
radio.dispatchEvent(new Event('change'));
|
||||||
});
|
});
|
||||||
|
var deleteBtn = item.querySelector('.video-delete-btn');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!confirm('Delete "' + v.filename + '"?')) return;
|
||||||
|
fetch('/api/videos/' + encodeURIComponent(v.filename), { method: 'DELETE' })
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (data.status === 'deleted') {
|
||||||
|
item.remove();
|
||||||
|
if (!videoList.querySelector('.video-item')) {
|
||||||
|
existingSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
videoList.appendChild(item);
|
videoList.appendChild(item);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -256,6 +256,32 @@
|
|||||||
color: var(--color-neutral-500);
|
color: var(--color-neutral-500);
|
||||||
}
|
}
|
||||||
.empty-state p { margin-bottom: var(--space-2); }
|
.empty-state p { margin-bottom: var(--space-2); }
|
||||||
|
|
||||||
|
/* Video delete button */
|
||||||
|
.video-delete-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--color-neutral-200);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-neutral-50);
|
||||||
|
color: var(--color-neutral-400);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.video-item:hover .video-delete-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.video-delete-btn:hover {
|
||||||
|
background: #FEE2E2;
|
||||||
|
color: #DC2626;
|
||||||
|
border-color: #FECACA;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user