장비에서 추론하면 생산 라인이 중앙의 응답을 기다리지 않아도 되고, 회선이 끊겨도 판정을 계속합니다. 중앙으로는 결과(작음)만 보내고 원본 영상(큼)은 보내지 않습니다. 결과는 다른 데이터와 같은 큐를 타므로, 끊긴 동안의 추론도 나중에 추론 결과 탭에 올라옵니다.

모델 레지스트리 ──pull──▶ models/{이름}/{버전}/ ──활성화──▶ 러너(YOLO·RF-DETR·직접 만든 것)

      카메라·검사 프로그램 ── POST /api/v1/inference ─────────▶ 판정 결과 (즉시 응답)

                                                     큐 ──▶ 중앙 '추론 결과' 탭

모델 받기(pull)

모델은 테넌트의 모델 레지스트리에 등록된 버전을 받습니다. 토큰에 models:read 스코프가 있어야 합니다. 세 가지 방법이 있고 모두 같은 일을 합니다. 받아서 풀고, 기본으로 바로 활성화하고, models.keep_versions(기본 2)보다 오래된 버전은 지웁니다.

디바이스 상세 명령 탭에서 모델 내려받기 를 고르고 모델과 버전을 선택해 명령 보내기 를 누릅니다. 장비가 다음 폴링 때 가져가 처리하고 결과를 보고합니다(명령·정책·보존).

성공하면 이런 모양의 결과를 돌려줍니다(값은 예시).

{ "name": "helmet-detector", "version": "2", "framework": "yolo", "activated": true, "pruned": ["1"] }

받은 모델은 data_dir/models/{이름}/{버전}/ 에 MLflow 모델 디렉터리 모양 그대로 풀립니다. 임시 폴더에 받은 뒤 이름을 바꾸므로, 중간에 끊겨도 반쯤 풀린 모델이 남지 않습니다.

활성화 규칙

  • 받을 때: activate 를 끄지 않으면 받은 버전을 바로 활성 모델로 만듭니다. 러너를 못 만들면(엑스트라 미설치 등) 받기는 성공으로 두고 활성화만 실패로 로그에 남깁니다.

  • 다시 켜질 때: 에이전트는 시작할 때 캐시에 있는 모델을 모두 불러 둡니다. 정전 뒤에도 명령을 다시 보내지 않아도 추론이 이어지게 하려는 것입니다. 모델이 여럿이면 이름·버전 순으로 마지막에 불린 것이 기본 활성 모델이 됩니다. 다른 모델을 쓰려면 요청에 model 을 지정하거나 :activate 로 고릅니다.

  • 버전 바꾸기:

    curl -X POST http://127.0.0.1:8600/api/v1/models/helmet-detector:activate \
         -H 'content-type: application/json' -d '{"version": "1"}'

어떤 러너가 도나

러너는 모델 디렉터리의 MLmodel 파일에서 metadata.geo_framework 를 읽어 고릅니다. 플랫폼 학습이 기록한 모델에는 이 값이 들어 있습니다.

geo_framework러너필요한 엑스트라
yolo내장 YOLO 러너yolo
rf-detr / rfdetr내장 RF-DETR 러너rfdetr
그 밖의 이름register_runner 로 등록한 러너없음

엑스트라가 없으면 no runner for framework 'yolo'; install 'geo-mlops-sdk[yolo]' 처럼 설치할 것을 알려 줍니다.

로컬 추론 API

POST/api/v1/inference

활성 모델로 이미지 한 장을 판정합니다. 세 가지 형식을 모두 받습니다.

curl -X POST http://127.0.0.1:8600/api/v1/inference -F [email protected]
# 모델 지정: -F model=helmet-detector

응답(실제 실행 결과):

{
  "task": "detect",
  "width": 640,
  "height": 480,
  "detections": [
    { "cls": 0, "name": "bright", "conf": 0.894, "bbox": [0.0, 0.0, 1.0, 1.0], "polygon": [] }
  ]
}
  • bbox[x1, y1, x2, y2] 이고 원본 이미지 기준 0~1 로 정규화돼 있습니다. polygon 은 분할 모델일 때만 채워집니다. 플랫폼의 중앙 서빙과 같은 모양이라, 모델을 장비에서 돌리든 중앙에서 돌리든 받는 쪽 코드를 바꿀 필요가 없습니다.
  • 활성 모델이 없으면 409 no active model; activate one first, 이미지가 없으면 400, 본문이 api.max_body_bytes 를 넘으면 413 입니다.
  • 기본으로 결과가 큐에 들어가 중앙의 추론 결과 탭에 모델·지연·결과와 함께 쌓입니다.
추론 결과 탭: 장비에서 돌린 추론의 모델·지연 시간·결과

커스텀 러너

플랫폼이 모르는 프레임워크(ONNX, OpenVINO, 규칙 기반 검사 등)는 Runner 프로토콜을 구현해 등록합니다. 메서드는 세 개입니다.

메서드하는 일
load(model: LocalModel)가중치 등을 불러 둔다. model.path, model.weights_path, model.class_names, model.metadata 를 쓸 수 있음
predict(image: bytes, **params) -> InferenceOutput이미지 한 장 판정
close()장치·메모리 해제

아래는 이미지 평균 밝기로 판정하는 간단한 예제 러너입니다. 실제로 돌려 위 응답과 추론 결과 탭 화면을 얻었습니다.

# my_edge.py
import io
import sys
from pathlib import Path

from PIL import Image

from geo_mlops_sdk.contracts.inference import Detection, InferenceOutput
from geo_mlops_sdk.edge.daemon import run
from geo_mlops_sdk.edge.models import register_runner
from geo_mlops_sdk.edge.settings import EdgeSettings


class BrightnessRunner:
    def load(self, model):
        self.classes = model.class_names or ["bright"]
        self.threshold = float(model.metadata.get("threshold", 100))

    def predict(self, image: bytes, **params) -> InferenceOutput:
        img = Image.open(io.BytesIO(image)).convert("L")
        mean = sum(img.getdata()) / (img.width * img.height)
        detections = []
        if mean >= float(params.get("threshold", self.threshold)):
            detections.append(Detection(cls=0, name=self.classes[0],
                                        conf=round(mean / 255, 3),
                                        bbox=[0.0, 0.0, 1.0, 1.0]))
        return InferenceOutput(task="detect", width=img.width,
                               height=img.height, detections=detections)

    def close(self):
        pass


register_runner("brightness", BrightnessRunner)   # 인자 없이 부를 수 있는 팩토리

if __name__ == "__main__":
    config = Path(sys.argv[1]) if len(sys.argv) > 1 else None
    sys.exit(run(EdgeSettings.load(config)))

모델 디렉터리의 MLmodel 에 프레임워크 이름을 적습니다.

# data_dir/models/brightness-check/1/MLmodel
flavors:
  python_function:
    loader_module: none
metadata:
  geo_framework: brightness
  geo_task: detect
  class_names: ["bright"]

python my_edge.py /etc/geo-mlops/edge.yaml 로 띄우면 캐시의 모델을 불러 활성화합니다.

geo-mlops-edge models
{
  "items": [
    { "name": "brightness-check", "version": "1", "framework": "brightness",
      "task": "detect", "classes": 1, "active": true }
  ],
  "active": "brightness-check"
}

러너의 load·predict 는 스레드 풀에서 돌고, 한 번에 한 예측만 돌도록 잠겨 있습니다. GPU 하나에서 두 예측이 겹쳐 메모리가 넘치는 일을 막기 위해서입니다.

2026-09-21 기준 플랫폼에 맞춰 작성했습니다.

© Geo-MLOps