모델과 추론
모델 레지스트리에서 모델을 받아 장비에서 추론하고, 결과를 중앙에 기록한다
장비에서 추론하면 생산 라인이 중앙의 응답을 기다리지 않아도 되고, 회선이 끊겨도 판정을 계속합니다. 중앙으로는 결과(작음)만 보내고 원본 영상(큼)은 보내지 않습니다. 결과는 다른 데이터와 같은 큐를 타므로, 끊긴 동안의 추론도 나중에 추론 결과 탭에 올라옵니다.
모델 레지스트리 ──pull──▶ models/{이름}/{버전}/ ──활성화──▶ 러너(YOLO·RF-DETR·직접 만든 것)
│
카메라·검사 프로그램 ── POST /api/v1/inference ─────────▶ 판정 결과 (즉시 응답)
│
큐 ──▶ 중앙 '추론 결과' 탭
모델 받기(pull)
모델은 테넌트의 모델 레지스트리에 등록된 버전을 받습니다. 토큰에 models:read 스코프가 있어야 합니다. 세 가지 방법이 있고 모두 같은 일을 합니다. 받아서 풀고, 기본으로 바로 활성화하고, models.keep_versions(기본 2)보다 오래된 버전은 지웁니다.
디바이스 상세 명령 탭에서 모델 내려받기 를 고르고 모델과 버전을 선택해 명령 보내기 를 누릅니다. 장비가 다음 폴링 때 가져가 처리하고 결과를 보고합니다(명령·정책·보존).
geo-mlops-edge models --pull helmet-detector --version-tag 2curl -X POST http://127.0.0.1:8600/api/v1/models/helmet-detector:pull \
-H 'content-type: application/json' \
-d '{"version": "2", "activate": true}'성공하면 이런 모양의 결과를 돌려줍니다(값은 예시).
{ "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-detectorcurl -X POST http://127.0.0.1:8600/api/v1/inference \
-H 'content-type: image/jpeg' --data-binary @frame.jpgcurl -X POST http://127.0.0.1:8600/api/v1/inference \
-H 'content-type: application/json' \
-d "{\"image_b64\": \"$(base64 -w0 frame.jpg)\", \"model\": \"\", \"record\": true, \"params\": {}}"JSON 에서만 record(결과를 중앙에 기록할지)와 params(러너에 넘길 값)를 줄 수 있습니다.
응답(실제 실행 결과):
{
"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 하나에서 두 예측이 겹쳐 메모리가 넘치는 일을 막기 위해서입니다.