장비에서 추론하면 생산 라인이 중앙의 응답을 기다리지 않고, 회선이 끊어져도 판정 계속. 중앙에는 결과(소용량)만 전송, 원본 영상(대용량)은 전송하지 않음. 결과는 다른 데이터와 같은 큐로 전송되므로 오프라인 동안의 추론 결과도 연결 복구 시 추론 결과 탭에 표시.

장비 내 추론: 검사 프로그램이 카메라 영상을 POST /api/v1/inference 로 러너에 전송하면 러너가 즉시 판정 반환. 러너는 중앙에서 내려받아 활성화한 models/{이름}/{버전}/ 의 모델 사용. 결과는 로컬 큐에 저장했다가 나중에 중앙의 추론 결과 탭으로 전송

모델 내려받기(pull)

모델은 테넌트의 모델 레지스트리에 등록된 버전을 내려받음. 토큰에 models:read 스코프 필요. 방법은 세 가지, 동작은 동일. 내려받아 압축 해제, 기본으로 즉시 활성화, 버전 번호가 큰 것부터 models.keep_versions(기본 2)개만 보관하고 나머지 버전은 삭제. 이미 캐시에 있는 버전이면 다시 내려받지 않고 활성화만 수행.

디바이스 상세 모델 탭에서 모델 배포 를 누르고 모델과 버전을 지정. 배포가 pull_model 명령을 보내고, 장비가 다음 폴링 때 가져와 처리한 뒤 하트비트로 그 버전을 실행 중이라고 보고하면 확인됨(Edge Fleet). 명령 탭의 모델 내려받기 도 같은 명령을 보내지만 배포 이력에 남지 않음.

성공하면 다음 결과 반환(값은 예시).

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

내려받은 모델은 data_dir/models/{이름}/{버전}/ 에 MLflow 모델 디렉터리 구조 그대로 압축 해제. 임시 폴더에 내려받은 뒤 이름을 변경하므로 중간에 중단되어도 일부만 풀린 모델이 남지 않음.

디바이스 상세 모델 탭의 이 버전으로 전환 도 이미 내려받은 버전으로 배포(같은 pull_model 명령). 캐시에 있는 버전이므로 다시 내려받지 않고 전환.

모델 명령

명령인자장비 동작
pull_modelname, version, activate(기본 true), deployment_id(선택)캐시에 없으면 내려받고, 활성화하고, keep_versions 초과분 삭제. 활성화에 실패하면 명령을 실패 로 보고(사유 포함)
activate_modelname, version캐시에 있는 버전으로 전환. 내려받지 않음. 캐시에 없으면 실패
remove_modelname, version캐시에서 그 버전 삭제. 실행 중인 버전이면 실패

내려받았지만 돌리지 못한 버전(엑스트라 없음, 가중치 로드 실패)은 디스크에 남고, 다음 하트비트부터 state: failed 와 error 로 보고. 중앙의 모델 탭에서 로드 실패 로 보이고 자세히 로 사유 확인.

활성화 규칙

  • 내려받을 때: activate 를 false 로 지정하지 않으면 내려받은 버전을 즉시 활성 모델로 지정. 러너를 생성하지 못하면(엑스트라 미설치 등) 명령은 실패 로 보고되고 중앙의 배포도 실패로 기록. 파일은 디스크에 남음.

  • 활성 포인터: 활성화에 성공한 버전은 data_dir/models/active.json 에 기록. 로컬 API :activate, CLI --activate, 중앙의 배포 모두 같은 파일을 갱신.

  • 재시작할 때: models.auto_activate 설정에 따름.

    값재시작 뒤
    pointer(기본)active.json 이 가리키는 버전만 로드해 활성. 파일이 없거나 그 버전이 캐시에 없으면 latest 로 동작
    latest모델 이름마다 버전 번호가 가장 큰 캐시 버전을 로드. 마지막에 로드된 모델이 활성
    none아무것도 로드하지 않음

    정전 뒤에도 명령을 다시 보내지 않고 추론 계속. 재시작 후 geo-mlops-edge models 로 확인.

  • 버전 전환:

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

    장비에서 직접 전환하면 중앙의 기대 모델과 달라져 모델 탭 배지가 불일치 로 바뀜. 중앙에서 다시 적용 하거나, 바꾼 버전을 이 버전으로 전환 으로 배포해 맞춤.

장비 프로그램이 직접 돌리는 모델

자체 검출기나 규칙으로 판정하는 프로그램도 플릿 입장에서는 모델을 돌리는 것. runtime.external_models 로 보고하면 하트비트 models[] 에 source: external 로 실리고, 중앙의 모델 탭에 외부 로 표시.

from geo_mlops_sdk.contracts.inference import ModelRef

runtime.external_models.set(
    ModelRef(name="pass-anomaly", version="rule-1"), framework="rule", task="pass_anomaly"
)
runtime.external_models.clear("pass-anomaly")   # 배포된 모델이 넘겨받을 때

외부 모델은 배포 대상이 아님. 배포된 모델로 바꾸려면 runtime.events 의 model.activated 를 받아 전환하고, model.deactivated·model.activation_failed 면 되돌림. 외부 모델은 하트비트에서 SDK 모델 뒤에 실리므로, SDK 모델이 활성이면 중앙은 그것을 장비의 실행 모델로 봄.

러너 선택

러너는 모델 디렉터리의 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.
  • 기본적으로 결과는 큐에 저장되고, 중앙의 추론 결과 탭에 모델·지연·결과와 함께 기록.
  • 판정한 프레임 자체를 학습 데이터로 보내려면 frames 수집기 사용.
추론 결과 탭: 장비에서 실행한 추론의 모델·지연 시간·결과

커스텀 러너

플랫폼이 지원하지 않는 프레임워크(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 에 프레임워크 이름 지정. 에이전트는 geo_framework 값을 소문자로 변환해 러너를 찾으므로 register_runner 의 이름은 소문자로 작성.

# 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,
      "path": "/var/lib/geo-mlops-edge/models/brightness-check/1", "active": true }
  ],
  "active": "brightness-check"
}

러너의 load·predict 는 스레드 풀에서 실행, 잠금으로 한 번에 한 예측만 실행. GPU 하나에서 두 예측이 동시에 실행되어 메모리가 부족해지는 것을 방지.

2026-10-02 기준 플랫폼으로 작성

© MLOps Central Platform