Gejala Masalah: OOM Killer pada Pipeline Ingestion RTSP

Background worker ingestion kamera RTSP yang menjalankan inferensi frame-by-frame sering mengalami degradasi stabilitas setelah beroperasi beberapa jam. Gejala paling umum terlihat pada metrik sistem: alokasi RAM proses naik secara linear tanpa pernah stabil, swap memory habis, dan kernel Linux mengeksekusi OOM (Out Of Memory) Killer untuk menghentikan proses worker secara paksa (dmesg: Killed process [pid] (python) total-vm:...).

Masalah ini jarang bersumber dari video decoder native (seperti FFmpeg atau OpenCV). Sumber utama kebocoran memori pada worker AI umumnya terjadi di layer Python runtime, tepatnya penumpukan objek tensor PyTorch yang gagal dilepaskan oleh garbage collector.

Root Cause: Computation Graph yang Menempel pada Metric Tensor

Secara default, operasi tensor PyTorch mengalokasikan memori untuk forward pass dan merekam dependensi kalkulasi untuk membangun dynamic computation graph (DAG autograd). Ketika worker memproses streaming kamera, developer sering mengumpulkan metrik deteksi, log scoring confidence, atau rata-rata loss ke dalam data structure global/worker state.

Jika tensor dimasukkan langsung ke list atau dictionary monitoring tanpa memutus dependensi autograd, seluruh chain tensor yang terlibat dalam kalkulasi frame tersebut tetap tertahan di memori. Objek graf tidak dapat dihancurkan oleh garbage collector Python karena root reference-nya masih aktif.

# CONTOH SALAH: Graph autograd terikat ke history worker
class StreamWorker:
    def __init__(self):
        self.confidence_history = []

    def process_frame(self, frame_tensor):
        output = model(frame_tensor)
        # output["confidence"] masih membawa relasi grad_fn dan referensi frame
        self.confidence_history.append(output["confidence"])

Identifikasi Root Cause dengan tracemalloc dan objgraph

Gunakan module standar tracemalloc untuk melacak lokasi baris kode yang mengalokasikan memori terbesar, dikombinasikan dengan objgraph untuk mendeteksi penumpukan tipe objek torch.Tensor.

1. Snapshot Alokasi Menggunakan tracemalloc

import tracemalloc

tracemalloc.start()
snapshot_awal = tracemalloc.take_snapshot()

# Simulasi proses ingestion worker
for _ in range(500):
    run_worker_step()

snapshot_akhir = tracemalloc.take_snapshot()
top_stats = snapshot_akhir.compare_to(snapshot_awal, "lineno")

for stat in top_stats[:5]:
    print(stat)

2. Menemukan Backreference Tensor Menggunakan objgraph

Gunakan objgraph untuk melihat apa yang menahan objek tensor di memory:

import gc
import torch
import objgraph

gc.collect()
# Tampilkan 5 objek teratas yang bertambah di memory
objgraph.show_growth(limit=5)

# Lacak apa yang menahan instance tensor terlama
tensors = [obj for obj in gc.get_objects() if isinstance(obj, torch.Tensor)]
if tensors:
    print(f"Active tensors in GC: {len(tensors)}")
    objgraph.show_backrefs(tensors[-1], max_depth=3, filename="tensor_chain.png")

Hasil visualisasi tensor_chain.png akan menunjukkan referensi melingkar dari list agregat metrik yang menahan tensor, lengkap dengan node AccumulateGrad atau BackwardNode yang mengikat frame tensor mentah di VRAM/RAM.

Solusi Perbaikan: Ingestion Worker Tanpa Kebocoran Memori

Terapkan dua langkah mutlak untuk service inferensi: nonaktifkan context autograd sepenuhnya menggunakan torch.inference_mode() (atau torch.no_grad()), dan ekstrak nilai numerik primitif sebelum logging/caching.

import torch
from typing import List

class SurveillanceWorker:
    def __init__(self, model: torch.nn.Module):
        self.model = model.eval()
        self.confidence_history: List[float] = []

    @torch.inference_mode()  # Memutus alokasi graph autograd secara total
    def process_frame(self, frame_tensor: torch.Tensor) -> None:
        # frame_tensor shape: (1, 3, H, W)
        outputs = self.model(frame_tensor)
        conf = outputs["confidence"]

        # BENAR: Konversi tensor skalar ke float murni via .item()
        # Jika tensor multi-dimensi, gunakan .detach().cpu().numpy() atau batasi copy
        self.confidence_history.append(conf.item())

        # Bersihkan buffer eksplisit jika list hanya untuk sliding window
        if len(self.confidence_history) > 1000:
            self.confidence_history.pop(0)

        # Hapus referensi lokal secara eksplisit
        del outputs
        del frame_tensor

Aturan Mutlak Produksi Worker Surveilans

  • Gunakan torch.inference_mode(): Lebih efisien dibanding torch.no_grad() karena mematikan tracking autograd sekaligus optimasi tensor version tracking.
  • Gunakan .item() untuk skalar: Jangan pernah menyimpan output tensor skalar langsung ke container Python (list/dict). Ambil float/int murninya.
  • Gunakan .detach() sebelum ekspor: Jika array output harus disimpan sementara, jalankan tensor.detach().cpu().clone() untuk membuang keterikatan graf.
  • Drain sliding window: Pastikan container history metrik memiliki hard limit ukuran elemen untuk menghindari unbounded memory growth.