저자: Guangyi Zhao
날짜: 2026-10-14
주제: AI-Node-Sentinel (저자의 오픈 소스 프로젝트, github.com/Ezra-Zhao/AI-Node-Sentinel), scaffold v0.1, Python 3.12.3
환경: Linux x86_64 VM, 저장소 코드는 수정 없이 실행; LLM 실험은 프로젝트 자체 프로토콜을 구현하는 로컬 가짜 공급자(fake providers)를 사용함 — 실제 모델 API는 호출되지 않음
1. 문제 및 배경
GPU 클러스터에서 고장 난 하드웨어의 첫 번째 신호는 보통 커널 로그의 한 줄, 즉 Xid입니다. XID는 NVIDIA가 GPU 하드웨어 및 펌웨어 오류에 대해 사용하는 번호 매기기 체계입니다 — PCIe 버스에서 떨어지는 GPU, 수정 불가능한 이중 비트 ECC 오류, NVLink 오류, GSP 펌웨어 시간 초과 등 각각 고유한 코드를 가집니다. 현장 대응 의식은 수동적입니다: 천 개의 GPU에서 dmesg를 가져와 각 코드가 무엇을 의미하는지 확인하고, 그 다음 결정합니다 — 노드를 비우거나(drain), GPU를 재설정하거나(reset), 재부팅하거나(reboot), 또는 RMA(Return Merchandise Authorization)를 시작합니다. 결정을 늦게 내리면 학습 작업이 고장 난 하드웨어에서 계속 재시도하여 실제 돈이 드는 GPU-시간을 소모하게 됩니다.
가장 당연한 충동은 이것을 LLM 에이전트에게 맡기는 것입니다. 모델이 원격 측정 데이터를 읽고, 오류 코드를 조회하며, 조치를 추천하도록 하는 것 — 이것이야말로 에이전트의 역할 아닌가요?
저 자신의 오픈 소스 프로젝트인 AI-Node-Sentinel도 그 아이디어를 중심으로 구축되었습니다: 원격 측정 데이터가 들어오면 XID 이벤트가 추출되고, 조회 테이블(lookup table)이 이를 풍부하게 만들고, 분류 규칙(triage rules)이 적용되며, 구조화된 보고서가 출력됩니다. 이 보고서에는 LLM 추론을 위한 LangChain 기반 슬롯이 예약되어 있습니다. 솔직한 현황부터 말씀드리자면, 모든 것은 이 단락에 달려 있으므로 알아주시기 바랍니다: 프로젝트는 scaffold v0.1이며, README에도 그렇게 명시되어 있습니다. 분류 결정 트리는 의도적인 더미(stub)입니다 — 모든 이벤트는 신뢰도 0.0으로 NEEDS_HUMAN_REVIEW를 받습니다. LangChain 공급자 역시 구성 시 NotImplementedError를 발생시키는 더미입니다. 모든 원격 측정 데이터는 번들된 시뮬레이터가 생성하는 합성(synthetic) 데이터입니다. 실제로 실행되는 단계는 수집(ingestion), 조회(lookup), 보고서 렌더링의 세 가지뿐입니다.
따라서 이것은 실제 운영 진단 시스템에 대한 기술 보고서가 아닙니다. 오히려 그 반대입니다. 저는 골격 구조를 해부 테이블 위에 올려놓고, LLM이 실제로 연결되었을 때 이 파이프라인이 어디에서 깨질지 측정하기 위해 여섯 가지 오류 주입(fault-injection) 실험을 실행했습니다. 그 결과는 운영 시스템에 LLM을 연결하는 모든 사람에게 일반화됩니다. 왜냐하면 여섯 개의 중단점(breakpoint) 중 어느 것도 모델의 역량 문제이기 때문이 아니기 때문입니다. 그것들은 배선 문제입니다. 일치하지 않는 출력 계약, 너무 적게 공급된 증거, 전체 배치(batch)를 오염시키는 하나의 잘못 구성된 레코드, 작업 디렉터리에 연결된 프롬프트 경로, 상태 비저장식 단일 이벤트 근시안(single-event myopia), 그리고 파이프라인 자체에 의해 폐기되는 상호 확인 신호들입니다. 더 강력한 모델은 이 문제 중 어느 것도 고치지 못합니다.
2. 방법론 및 아키텍처
코드베이스는 작습니다. 테스트와 데모를 포함하여 대략 470줄의 여덟 개 핵심 Python 파일로 구성되어 있습니다. 네 부분으로 나뉩니다. agent/schemas.py는 데이터 타입을 정의합니다: TelemetryEvent(출처, 종류 및 페이로드가 있는 정규화된 이벤트), XidFinding(조회로 풍부해진 XID 이벤트), 그리고 TriageVerdict(조치, 신뢰도, 근거). tools/xid_lookup.py는 코드를 이름, 카테고리, 심각도로 매핑하는 정적 테이블이며, NVIDIA의 공개 XID 문서와 Google Cloud의 GPU 문제 해결 가이드에서만 시드됩니다. 누락된 값은 None을 반환하며, 절대 추측하지 않습니다. tools/triage_rules.py는 의사 결정 트리(decision tree)를 담고 있으며, 현재는 스텁입니다. agent/orchestrator.py는 TriageAgent로, XID 이벤트를 수집하고 추출하며, 각각을 분류하고, 보고서를 생성합니다.
LLM 통합 지점은 agent/llm.py에 위치하며, 최소한의 프로토콜인 LLMProvider를 요구합니다. 이 프로토콜은 reason(system_prompt, evidence) -> str 메서드 하나만 필요로 합니다. 오케스트레이터는 먼저 결정론적 판결을 계산합니다. 만약 제공자(provider)가 존재하면, 증거 문자열을 구축하고, 제공자를 호출하며, 반환된 텍스트를 [LLM] 접두사를 붙여 판결의 근거(rationale)에 추가합니다. prompts/triage_system.md에 있는 초안 시스템 프롬프트는 모델에게 주어진 증거만을 인용하도록 지시하고, 코드가 알려지지 않았을 때 명시적으로 말하도록 하며, 0과 1 사이의 신뢰도와 함께 정확히 하나의 액션을 반환하도록 하고, 신뢰도가 0.7 미만일 때는 무조건 NEEDS_HUMAN_REVIEW를 강제하며, JSON 형식으로 답변하도록 지시합니다.
방법: 수정되지 않은 리포지토리를 대상으로 다섯 가지 실험 그룹을 진행했으며, 스크립트와 원본 JSON은 이 기사와 함께 보관되어 있습니다. (1) 기준선 규모(Baseline scale): 번들된 시뮬레이터는 4개, 16개, 64개의 노드에 대한 원격 측정(telemetry) 창을 생성합니다. 저는 이벤트 구성, 판결 분포, 파이프라인 지연 시간을 기록했습니다. (2) 조회 범위(Lookup coverage): 테이블 크기 및 테이블 외의 탐색 코드 10개를 포함합니다. (3) 잘못된 데이터 주입(Malformed-data injection): 누락된 xid 키를 가진 이벤트, 비숫자형 xid, 그리고 xid=None 이벤트를 그 외 정상적인 배치에 혼합했습니다. (4) 가짜 LLM 실험(Fake-LLM experiments): 프롬프트가 요구하는 JSON을 정확히 반환하는 로컬 제공자를 사용하여 판결에서 실제로 무엇이 바뀌는지 관찰한 후, 항상 오류를 발생시키는 제공자를 사용하여 장애 전파(fault propagation)를 관찰했습니다. (5) 실패 모드 탐색(Failure-mode probes): 하나의 GPU에 반복적인 오류를 주거나, 상호 확증적 이벤트의 운명, 그리고 다른 작동 디렉토리에서 프롬프트 로딩을 시도했습니다.
3. 측정값
3.1 기준선: 결정론적 부분이 너무 빨라 중요하지 않고, 판결은 스텁(stub)에 의존합니다
| Scale | Events | XID events | Verdicts | Unknown verdicts | Severity (critical/high/unknown) | Pipeline time |
|---|
| 4 nodes × 8 GPUs (seed 42) | 76 | 12 | 12 | 1 | 10 / 1 / 1 | ~0.1 ms |
| ... | | | | | | |
| 세 가지 관찰 결과가 있습니다. 첫째, 모든 판정(verdict)이 신뢰도 0.0인 NEEDS_HUMAN_REVIEW입니다. 이는 측정된 지능이라기보다는 더미 동작(stub behavior)이며, 이후 내용을 위한 대조군으로 기록합니다. 둘째, 약 1,300개의 이벤트가 밀리초(millisecond) 미만으로 처리됩니다. 원시 단일 실행 perf_counter 타이밍으로는 초당 약 177만 개의 이벤트입니다 (표에서는 0.1ms로 반올림되었으며, 밀리초 미만의 수치는 크기 차수(order-of-magnitude)일 뿐 정확한 벤치마크는 아닙니다). 이는 조회(lookup)가 사전(dictionary) 읽기이기 때문입니다. 이 기사의 규모에서는 결정론적 부분은 비용이 들지 않습니다. 스트리밍 수집(streaming ingestion)과 윈도우 상태(window state)는 나중에 별도의 측정이 필요하겠지만, 미래의 지연 시간(latency)과 비용 대부분은 LLM 단계에서 발생할 것입니다. 셋째, 11–13%의 알 수 없는 비율을 오해해서는 안 됩니다. 시뮬레이터의 주입 풀(injection pool)에는 여덟 개의 코드가 있으며, 그중 하나(999)는 의도적으로 합성된 미지 코드이므로, 알려지지 않은 비율은 필드에서 측정된 실제 플릿(fleet)의 미지 코드를 의미하는 것이 아니라 풀 설계의 산물입니다. | | | | | | |
저장소에 포함된 여섯 가지 테스트 모두 직접 호출할 때 통과합니다 (결정론적 동작, 알려진 코드 조회, 추측되지 않은 미지 코드, 인간 검토를 위한 더미 라우팅, 보고서 형태). 이 머신에는 pytest가 설치되어 있지 않아, 정확성을 위해 러너(runner)를 통해 실행하는 대신 테스트 함수들을 직접 실행했습니다.
3.2 조회 커버리지: 일곱 개의 코드가 있고, 나머지는 모두
테이블에는 정확히 일곱 개의 코드(13, 31, 48, 74, 79, 119, 120)가 포함되어 있습니다. 테이블 외의 열 개 프로브 코드(43, 45, 56, 63, 64, 68, 92, 94, 109, 154)는 모두 None을 반환하며, 이는 정확히 999와 동일하게 처리됩니다: 심각도 알 수 없음(severity unknown), 알려지지 않음(known=False), 인간 검토로 라우팅됨. 이 라우팅 동작은 옳습니다. 추측하지 않는 것이 이 파이프라인의 가장 중요한 안전 속성입니다. 커버리지는 또 다른 문제입니다. 그 일곱 개 코드 외의 모든 XID는 운영자에게 정확히 하나의 정보, 즉 "알 수 없는 오류가 발생했으니 확인해 보세요"를 제공합니다. 이 테이블은 공개 문서를 기반으로 수동으로 채워졌으며, 이를 확장하는 것은 영광스럽지 못한 작업이고, 섹션 4.3에서 여전히 고쳐야 할 첫 번째 문제가 아니라고 주장합니다.
오케스트레이터는 int(event.payload["xid"] ) 코드를 추출하며, 유효성 검사나 개별 이벤트 격리(per-event isolation)가 없습니다. 형식이 잘못된 이벤트를 좋은 배치에 섞을 경우:
| 주입(Injection) | 결과(Outcome) |
|---|
| 잘 구성된 단일 이벤트 | 완료됨, 판결 1건 |
| ... | |
run()은 리스트 컴프리헨션(list comprehension)으로 이벤트를 처리하므로, 첫 번째 예외가 발생하면 창(window) 내의 모든 판결을 폐기합니다. 프로덕션 환경에 적용하자면, dmesg 파서가 결국 필드가 불완전한 이벤트(field-incomplete event)를 방출할 것이고, 이때 실패 모드는 "하나의 이벤트만 진단되지 않은 것"이 아니라 "프로세스 레벨 예외로 인해 전체 창의 판결이 사라지는 것"입니다. 나쁜 기록 카운터와 함께하는 개별 이벤트 격리는 이러한 종류의 파이프라인에서 기본적으로 갖춰야 할(table stakes) 요소이며, 여기에는 부재합니다. | |
3.4 가짜 LLM 실험: 출력 계약이 깨지고 모델은 투표를 받지 못한다
이것이 이 글이 존재하는 실험입니다. 저는 시스템 프롬프트에 완벽하게 복종하며 XID 79(GPU가 버스에서 떨어짐)에 대해 신뢰도 0.95로 DRAIN_NODE를 추천하는 JSON을 반환하는 가짜 제공자(fake provider)를 구현했습니다. 오케스트레이터에 연결했을 때, 판결의 실제 상태는 다음과 같습니다:
| Field | Fake provider output | Final verdict |
|---|
| action | DRAIN_NODE | NEEDS_HUMAN_REVIEW (변경 없음) |
| ... | | |
| The 코드는 그 이유를 명확히 합니다. 프로토콜은 문자열을 반환하고, 오케스트레이터는 그것을 단순히 연결할 뿐이며, JSON을 파싱하는 것은 아무것도 없고, 결정론적인 판결을 덮어쓰는 것도 없습니다. 프롬프트의 출력 형식과 신뢰도 게이트(confidence gate)는 모델과의 일종의 약속일 뿐이며, 코드 측에서는 결코 지키지 않습니다. 설령 실제 LangChain provider가 내일 구현되고 모델이 매번 완벽하게 형식을 따르더라도, 분류(triage) 결정은 여전히 stub에 의해 100% 결정될 것이며; LLM의 총 영향력은 근거(rationale) 안에 붙여넣어진 한 단락에 불과할 것입니다. | | |
실험의 후반부는 증거의 두께(evidence thickness) 문제입니다. 오케스트레이터가 조립하는 정확한 증거 문자열은 다음과 같습니다: node=node-00 gpu=0 xid=79 name=GPU has fallen off the bus severity=critical ts=. 이것이 전부입니다. 조회 항목에는 복구 힌트(remediation hints)와 출처 노트(sourcing notes)를 포함하는 category 및 notes 필드가 있지만, 이들 중 어느 것도 증거에 포함되지 않습니다. 동일한 GPU의 온도 및 ECC 판독값 역시 포함될 수 없습니다. 왜냐하면 증거는 이벤트별 분류 함수 내에서 구축되는데, 이 함수의 시그니처에는 창(window) 컨텍스트가 전혀 없기 때문입니다. 프롬프트의 첫 번째 규칙은 모델에게 제공된 증거만 인용하도록 지시합니다. 파이프라인은 증거를 오류 이름의 재진술 수준으로 고갈시킨 다음, 모델이 그 이상으로 나아가는 것을 금지합니다. 만약 모델이 어쨌든 정보가 풍부한 답변을 한다면, 그것은 규칙을 위반함으로써만 가능할 것입니다. 이 모순은 모델 결함이 아니라 배선(wiring) 결정입니다.
세 번째: 오류 전파(fault propagation). 제공자(provider)를 항상 RuntimeError를 발생시키는 것으로 교체하고, triage_event가 예외를 그대로 통과시키도록 하면 — 전체 실행이 종료됩니다. 이는 섹션 3.3에서와 같은 방식의 종료입니다. 제공자는 외부 종속성(external dependency)이며, 시간 초과(timeouts), 요청 제한(rate limits), 네트워크 지터(network jitter)는 그 정상적인 환경적 요인입니다. 선택적인 추론 단계(optional reasoning step)는 결정론적 판정(deterministic verdict)으로 저하되어야 하며, 배치 전체를 함께 다운시키지 않아야 합니다. 여기에는 성능 저하 경로가 없습니다.
3.5 프롬프트 경로는 작업 디렉터리에 결합됨
오케스트레이터는 상대 경로로 프롬프트를 로드합니다(open("prompts/triage_system.md")). 리포지토리 루트에서는 작동하지만, /tmp에서는 동일한 코드가 즉시 FileNotFoundError를 발생시킵니다. (LangChain 제공자가 생성 시 NotImplementedError를 발생시키는 것은 결함이 아니라 정직한 스텁입니다.) 상대 경로는 프로덕션 환경에서 정확히 한 번 문제를 일으키는 종류의 버그입니다: systemd, 컨테이너 또는 스케줄러에 의해 시작된 서비스들은 작업 디렉터리로 리포지토리 루트를 갖는 경우가 거의 없지만, 개발자의 터미널은 항상 그렇습니다 — 이것이 바로 책상에서 재현되지 않는 이유입니다.
3.6 메모리 부재와 상호 검증의 맹점: 하나의 근본 원인으로 인한 두 가지 실패 모드
반복되는 오류는 에스컬레이션하지 않습니다. 같은 GPU에서 3시간 이내에 발생한 세 번의 XID 79 이벤트는 서로 아무것도 들어본 적 없는 세 개의 판정을 생성합니다: 동일한 조치, 동일한 신뢰도, 동일한 근거. 인과관계(Causality)를 정확히 말하자면: 동일한 판정은 결정 트리(decision tree)가 스텁이기 때문에 직접적으로 발생하며 — 어떤 아키텍처라도 오늘날 이렇게 만들 수 있습니다. 아키텍처적 문제는 한 단계 아래에 놓여 있습니다. 이 스텁 자체의 주석은 의도된 실제 규칙을 스케치합니다 — XID 79가 24시간 동안 한 GPU에서 두 번 발생하면 드레인(drain) 및 재부팅(reboot)을 의미해야 한다고 되어 있지만, 오케스트레이터는 이벤트를 상태 비저장(statelessly)으로 하나씩 판정하기 때문에, 설령 그 규칙이 작성된다 하더라도 파이프라인은 그것을 넣을 곳이 없습니다. 이것은 단순히 누락된 규칙일 뿐만 아니라, 누락된 상태입니다.