dev.to

WiFi off, model running: what broke (and what shipped) building YOLO26 on MLX

WiFi 끈 채로 모델 구동 — MLX 위에 YOLO26을 올리며 깨진 것과 출시된 것

2인 팀이 7일 만에 Apple Silicon MLX 위에서 네트워크 없이 동작하는 온디바이스 자세 감지 맵 'SENTINEL'을 만든 빌드 노트입니다. 데모 자체보다 macOS 26의 AVFoundation 크래시, yolo-mlx의 지연 평가(lazy eval) 데드락, 파인튜닝 붕괴라는 세 가지 실패 모드의 진단 과정이 핵심입니다. yolo26n 가중치를 그대로 쓰고 바운딩 박스 종횡비 휴리스틱만 얹어 M4에서 720p 16FPS를 달성했습니다.

본문

webAI YOLO26 MLX Build Challenge(2026년 5월)에 참가한 2인 팀이 7일간의 개발 과정을 정리한 빌드 노트입니다. 완성된 SENTINEL은 WiFi를 물리적으로 끈 상태에서 동작하는 단일 파일 온디바이스 '자세 주의(attention) 맵'으로, 현장 대응자(responder)가 환자가 많고 손이 부족한 상황에서 누구에게 먼저 주의를 기울여야 할지 시각적 단서를 제공하는 것이 목표입니다. 공학은 Igor Eduardo가, 문제 정의와 운용 제약은 현장 보안 경력의 Lexi Armstrong이 맡았습니다.

출시된 것: 사양과 설계

출시 버전은 공개된 yolo26n 가중치를 그대로 사용하며, 그 위에 결정론적 자세 휴리스틱만 얹은 약 200라인의 단일 파일 Python 프로그램입니다. 단일 스레드 동기 루프로 M4 / 32GB / macOS 26.3 / Python 3.14 환경에서 720p 기준 약 16FPS, 프레임당 추론 약 45ms를 기록했습니다. 휴리스틱은 class == 0(person)이고 confidence 0.40 이상인 검출에 대해 바운딩 박스의 세로/가로 비율(aspect)을 계산해, 1.6 초과면 서 있음(standing), 1.0~1.6이면 앉음/웅크림(sitting), 1.0 미만이면 누움(lying)으로 분류합니다. 카메라가 맥박이나 기도를 볼 수 없으므로 의학적 중증도가 아니라 '누가 서 있고 누가 쓰러져 있는가'만 보고하며, 최종 판단은 사람이 하는 구조입니다.

실패 1: macOS 26 + OpenCV 4.13 워커 스레드 캡처 크래시

초기 아키텍처는 FastAPI 백엔드가 WebSocket으로 브라우저 오버레이에 스트리밍하고, ThreadPoolExecutor에서 카메라 캡처를 돌리는 구조였습니다. 그러나 OpenCV 4.13의 AVFoundation 백엔드에서 비-메인 스레드 캡처가 SIGTRAP으로 확실하게 크래시했고, 트레이스는 cv2.abi3.so의 CaptureDelegate captureOutput에서 CFRelease로 이어졌습니다. macOS 26에서 AVCaptureSession을 메인 스레드 밖에서 소유할 때 발생하는 참조 카운팅 문제로 보이며, 7일이라는 기간 안에서 캡처 스택을 재작성하는 리스크를 감수하지 않고 회피를 택했습니다. 워커 스레드 캡처 패턴은 대부분의 데모가 가르치는 방식이므로 같은 스택을 쓰는 사람은 반드시 부딪힐 문제라고 경고합니다.

실패 2: yolo-mlx Boxes의 지연 평가 데드락

더 분리하기 어려웠던 버그입니다. 벤치마크는 깨끗하고 워밍업도 완료됐지만 WebSocket 핸들러가 실제 카메라 프레임으로 detector.predict()를 호출하면 매번 조용히 멈췄습니다. 추론을 스텁으로 대체하니 영상이 즉시 나와 카메라·WebSocket·프론트엔드는 정상이었고, yolo-mlx 내부에 출력문을 넣어 보니 모델 반환 이후 박스 반복 루프, 정확히는 box.xyxy[0].tolist() 지점에서 멈추는 것을 확인했습니다. 원인 가설은 Boxes의 .xyxy/.conf/.cls가 지연 평가되는 MLX 프록시라는 점입니다. 추론 중에는 평가되지 않다가 반복 루프에서 .tolist() 호출 시 2차 mx.eval()이 발생해 멀티스레드 환경에서 데드락에 빠진 것입니다. 워밍업이 np.zeros(검출 0건)로 이뤄져 박스 루프가 실행되지 않았기에 실제 피사체가 프레임에 들어올 때까지 보이지 않는 버그였습니다. 우회법은 반복 전에 mx.eval(boxes.xyxy) 등으로 강제 구체화(materialize)한 뒤 np.array로 변환해 NumPy 위에서 반복하는 것입니다.

실패 3: 파인튜닝 붕괴와 아키텍처 피벗

Roboflow 자세 분류 데이터셋(person_lying / person_sitting / person_standing)으로 yolo26n 파인튜닝도 시도했지만 학습 손실이 epoch 2에서 거의 0으로 붕괴하고 mAP가 남은 epoch 내내 0.0에 머물렀습니다. 근본 원인은 레이블 형식 불일치나 정규화 갭일 가능성이 크지만 시간 내에 분리하지 못했고, '학습된 자세 헤드'는 로드맵에 있는 열린 과제임을 명시했습니다. 4일차에는 생산급 복잡성 대신 데모를 택해 공식 스타터를 포크한 뒤 추론 호출 주변을 모두 지우고 메인 스레드 단일 동기 루프(cap.read() → model(frame) → 오버레이 렌더 → cv2.imshow)로 재작성했습니다. 이 피벗이 AVFoundation 문제(메인 스레드 캡처)와 지연 평가 문제(비동기·스레딩 제거)를 동시에 우회했습니다.

휴리스틱의 한계와 후속 과제

저자들은 휴리스틱의 실패 조건도 솔직하게 적습니다. 상반신만 잡힌 근접 촬영의 서 있는 사람은 박스가 정사각형에 가까워 '앉음'으로 읽히고, 태아 자세로 누운 경우도 오분류되며, 두 사람이 앞뒤로 겹치면 하나의 박스가 다른 하나를 삼킵니다. 잘된 점은 yolo-mlx 0.3.1 API, Apple Silicon의 MLX Metal 백엔드, 스타터 리포 구조 등이고, 마찰은 위 두 버그 외에 OpenCV Hershey 폰트가 ASCII 전용이라 중간점 구분자가 ??로 렌더된 점 등이었습니다. 후속으로는 학습된 자세 헤드, 로컬 메시 기반 멀티카메라 융합, iOS/iPadOS 포팅을 들고, RGB rPPG는 현장 조건이 성립하지 않아 연구용으로만 두며 실제 바이탈 경로는 열화상이라고 봤습니다. 폼팩터로는 Vision Pro 프로토타입과 현장용 웨이브가이드를 스케치했으나, 페어링 퍽(puck)으로 연산을 넘기는 플랫폼은 제로 이그레스(zero egress) 스토리를 약화한다고 지적합니다. 제품이 되기 전에 학습된 분류기, 실제 레이블 데이터셋, 현장 대응자 검증이 필요하며 시장 전망은 아직 내놓을 자격이 없다고 선을 긋습니다.

원문: dev.to / 번역·요약: Trawling