Diagnosa Gejala: OOM Killer pada Worker Queue

Model Text-to-CAD berbasis LLM kerap menghasilkan kode konstruksi parametrik (seperti OpenSCAD atau B-Rep step commands) dengan kompleksitas struktural tak terduga. Pada sistem pemrosesan asinkron via message queue (misal Celery atau BullMQ), worker backend tiba-tiba dihentikan oleh Linux Kernel dengan sinyal SIGKILL (Exit code 137).

Pemeriksaan kernel buffer via dmesg -T mengonfirmasi aktivasi Out-of-Memory (OOM) killer:

[Thu Oct 24 10:14:02 2024] Out of memory: Killed process 41209 (cad_worker) total-vm:4194304kB, anon-rss:2097152kB, file-rss:128kB, shmem-rss:0kB
[Thu Oct 24 10:14:02 2024] oom_reaper: reaped process 41209 (cad_worker), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Analisis Root Cause: Unbounded AST Parsing

Masalah bukan berada pada CAD kernel rendering (seperti OpenCASCADE engine), melainkan pada fase parsing kode sumber ke Abstract Syntax Tree (AST). Pipeline parsing awal memuat seluruh token ke heap memory sebelum evaluasi geometri dijalankan.

Dua penyebab struktural utama pada sintaks yang dihasilkan LLM:

  • Unbounded Loop Expansion: LLM menghasilkan loop bersarang (nested loops) dengan batas iterasi masif tanpa validasi numerik, misalnya evaluasi for (i = [0:1000]) for (j = [0:1000]) yang memicu instansiasi jutaan AST node geometri primitif.
  • Deep Recursive CSG Expansion: Evaluasi operasi Constructive Solid Geometry (CSG) rekursif tanpa dynamic depth limiting mengakibatkan ledakan tree size di heap, melampaui alokasi memori proses worker.

Strategi Remediasi

1. Streaming Tokenizer dan AST Traversal Guard

Validasi dilakukan langsung saat traversal tree sebelum proses evaluasi CAD native dijalankan. Batasi total alokasi node dan kedalaman nesting secara eksplisit.

class ASTValidationError(Exception): pass

class GuardedASTVisitor:
    def __init__(self, max_depth=32, max_total_nodes=5000):
        self.max_depth = max_depth
        self.max_total_nodes = max_total_nodes
        self.current_depth = 0
        self.node_count = 0

    def visit(self, node):
        self.node_count += 1
        if self.node_count > self.max_total_nodes:
            raise ASTValidationError(f"Exceeded maximum node allocation budget: {self.max_total_nodes}")

        self.current_depth += 1
        if self.current_depth > self.max_depth:
            raise ASTValidationError(f"Exceeded maximum AST depth limit: {self.max_depth}")

        try:
            # Simpan tipe ekspresi geometri dan loop bounds
            if node.get("type") == "ForLoop":
                start, end, step = node.get("range", (0, 0, 1))
                iterations = abs((end - start) // step)
                if iterations > 100:
                    raise ASTValidationError(f"Loop iteration count {iterations} exceeds limit (100)")

            for child in node.get("children", []):
                self.visit(child)
        finally:
            self.current_depth -= 1

ponytail: Visitor logic disederhanakan ke dict schema; gunakan parser-specific visitor hooks jika memigrasikan ke Lark/ANTLR.

2. Limitasi Resource pada Level Subproses dan cgroups

Jangan biarkan worker crash mengganggu keseluruhan runtime. Eksekusi evaluasi parsing CAD dalam child process terisolasi dengan batas memori virtual menggunakan resource di level OS atau cgroups v2 di level container.

import resource
import sys

def enforce_worker_memory_limit(max_bytes=512 * 1024 * 1024):
    # Set soft and hard limit untuk Virtual Memory (RLIMIT_AS)
    resource.setrlimit(resource.RLIMIT_AS, (max_bytes, max_bytes))

def parse_cad_payload(payload):
    # Eksekusi fungsi ini via multiprocessing child process
    enforce_worker_memory_limit()
    # Parser logic berjalan di sini
    pass

Untuk container deployment (Docker/Kubernetes), definisikan cgroup hard limits pada manifest worker pod untuk mencegah degradasi host node:

resources:
  limits:
    memory: "1Gi"
    cpu: "1000m"
  requests:
    memory: "512Mi"
    cpu: "500m"

Uji Validasi: Unit Test Pembatas Resource

Pengujian mandiri tanpa framework berat untuk memastikan payload berbahaya berhasil digagalkan sebelum memicu OOM:

def test_ast_guard():
    # Struktur rekursif melebihi batas kedalaman 32
    root = current = {"type": "Union", "children": []}
    for _ in range(40):
        child = {"type": "Difference", "children": []}
        current["children"].append(child)
        current = child

    visitor = GuardedASTVisitor(max_depth=32, max_total_nodes=5000)
    try:
        visitor.visit(root)
        assert False, "Test failed: AST depth guard did not raise ASTValidationError"
    except ASTValidationError as err:
        assert "Exceeded maximum AST depth limit" in str(err)

test_ast_guard()

Trade-offs dan Kesimpulan

Membatasi resource depth dan loop unrolling menghentikan OOM crash secara deterministik, namun dapat memicu false positive pada model 3D valid yang sangat detail (seperti lattice structures atau thread mekanikal padat). Solusinya, alokasikan pipeline khusus (heavy tier queue) dengan validasi manual atau resource pool lebih tinggi untuk model kompleks.