fix: add explicit GPU memory cleanup between models to prevent OOM

This commit is contained in:
jetson committed 2026-09-21 10:19:29 +07:00
1 parent 6783ff3970
commit 93daa68668
6 files changed
+109 -5

No files matched your search

+13
View File
@@ -196,6 +196,19 @@ def api_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")
def api_models():
groups = scan_model_groups(MODELS_DIR)
+11
View File
@@ -28,6 +28,17 @@ class BaseDetector:
self._conf = conf
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]:
results = self._model.predict(
frame, conf=self._conf, verbose=False
+9
View File
@@ -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:
if job.status != JobStatus.CANCELLED:
job.status = JobStatus.COMPLETED
+8
View File
@@ -235,6 +235,14 @@ def run_pipeline(
cap.release()
if writer is not None:
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
return PipelineResult(
+42 -5
View File
@@ -132,6 +132,23 @@
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
------------------------------------------------ */
@@ -205,11 +222,12 @@
var item = document.createElement('label');
item.className = 'video-item';
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-name">' + escapeHtml(v.name) + '</div>' +
'<div class="video-item-meta">' + formatBytes(v.size) + '</div>' +
'</div>';
'<div class="video-item-name">' + escapeHtml(v.filename) + '</div>' +
'<div class="video-item-meta">' + formatBytes(v.size) + ' &middot; ' + formatRelativeTime(v.mtime) + '</div>' +
'</div>' +
'<button type="button" class="video-delete-btn" title="Delete video" data-filename="' + escapeHtml(v.filename) + '">&#128465;</button>';
var radio = item.querySelector('input[type="radio"]');
radio.addEventListener('change', function () {
// Deselect all other items
@@ -221,7 +239,7 @@
selectedExistingVideo = v.path;
videoPathInput.value = v.path;
// Show in file-info
fileName.textContent = v.name;
fileName.textContent = v.filename;
fileSize.textContent = formatBytes(v.size);
fileDuration.textContent = '—';
fileInfo.classList.add('visible');
@@ -237,9 +255,28 @@
});
item.addEventListener('click', function (e) {
if (e.target.tagName === 'INPUT') return;
if (e.target.classList.contains('video-delete-btn')) return;
radio.checked = true;
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);
});
})
+26
View File
@@ -256,6 +256,32 @@
color: var(--color-neutral-500);
}
.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>
{% endblock %}