시작하기
기본 개념
데이터와 모델 파일을 필요한 장비로 자동 전송하기
AI 업무에서는 원본 데이터, 전처리 결과, 학습 데이터, 모델 파일, 추론 결과 등 다양한 파일이 여러 장비와 환경에서 사용됩니다.
각 파일이 생성되는 위치와 사용하는 장비를 연결하면, 데이터가 준비된 이후 다음 처리 환경으로 전송하고 생성된 모델과 결과 파일도 필요한 위치로 이어갈 수 있습니다.
Data Source
│
▼
Data Processing
│
▼
AI Training
│
▼
Model Output
│
▼
Result Storage

이렇게 구성하면 AI 데이터와 모델 파일을 각 단계의 처리 환경에 맞춰 관리하고, 파일이 준비되는 시점에 다음 작업으로 자동 연결할 수 있습니다.
워크플로 흐름
데이터 수집부터 처리와 결과 활용까지 연결하기
여러 장비에서 생성되는 데이터를 수집한 후 필요한 AI 처리 환경으로 전송합니다.
데이터 처리와 학습이 완료되면 생성된 모델과 결과 파일을 다음 장비 또는 저장 환경으로 전송해 후속 업무에 활용합니다.
데이터 생성
│
▼
데이터 수집
│
▼
전처리 · AI 처리
│
▼
모델 생성
│
├──────────────┐
▼ ▼
모델 저장 결과 전달
│ │
└──────┬───────┘
▼
다음 작업
각 단계는 앞선 작업에서 생성된 파일과 처리 결과를 다음 단계의 입력으로 활용할 수 있습니다.
자동화 효과
단계마다 반복되는 대용량 파일 이동과 관리 작업 줄이기
AI 업무에서는 데이터의 크기와 처리 단계가 늘어날수록 파일을 준비하고 필요한 장비로 전송하는 작업도 함께 증가합니다.
파일 생성과 처리 상태를 기준으로 다음 작업을 자동 실행하도록 구성하면, 각 단계의 파일 흐름을 정해진 순서에 따라 연결할 수 있습니다.
| 구분 | 개별 파일 관리 | 워크플로 자동화 |
|---|---|---|
| 데이터 수집 | 장비별 파일 확인 | 여러 장비의 데이터를 하나의 흐름으로 수집 |
| 처리 환경 연결 | 다음 장비로 파일 준비 | 처리 조건에 따라 필요한 장비로 자동 전송 |
| 모델 관리 | 생성된 모델 파일 확인 | 모델 생성 후 지정된 환경으로 연결 |
| 결과 활용 | 처리 결과를 다음 업무에 준비 | 결과 파일을 다음 작업과 자동 연결 |
이를 통해 데이터, 모델, 결과 파일의 흐름을 단계별로 구성하고 AI 작업 전체를 하나의 파일 워크플로로 관리할 수 있습니다.
IT 엔지니어
AI 데이터와 모델 파일의 자동화 흐름 구성하기
데이터 수집
여러 장비의 데이터를 하나의 흐름으로 모으기
먼저 AI 처리에 사용할 데이터가 생성되거나 저장되는 여러 장비를 워크플로에 연결합니다.
각 장비의 파일 경로와 수집 대상을 설정하면 여러 위치의 데이터를 하나의 처리 흐름으로 모아 다음 단계에서 활용할 수 있습니다.
Source A ──┐
│
Source B ──┼──→ Data Collection
│
Source C ──┘
│
▼
AI Processing
[제품UI_여러 Source 장비와 데이터 폴더를 하나의 데이터 수집 작업으로 연결하는 Flow Canvas 화면]
필요한 경우 파일 경로와 이름, 확장자 등의 조건을 설정해 AI 처리에 사용할 데이터만 수집할 수 있습니다.
처리 연결
수집된 데이터를 필요한 AI 처리 장비로 전송하기
수집한 데이터가 준비되면 전처리, 학습, 추론 등 다음 작업을 수행할 AI 처리 장비로 연결합니다.
각 처리 단계에서 사용할 장비와 파일 경로를 지정하고, 앞선 단계의 결과 파일이 다음 장비의 입력으로 이어지도록 구성합니다.
Collected Data
│
▼
Preprocessing
│
▼
Training Server
│
▼
Inference / Analysis

이렇게 구성하면 각 AI 처리 단계에서 필요한 파일을 준비된 작업 환경으로 자동 전송할 수 있습니다.
작업 조건
데이터와 처리 상태에 따라 다음 작업 실행하기
워크플로의 각 단계는 파일 생성, 데이터 수집 완료, 처리 완료 등 설정한 조건을 기준으로 다음 작업을 시작하도록 구성할 수 있습니다.
예를 들어 여러 장비에서 수집할 데이터가 준비된 후 전처리 작업을 시작하고, 전처리 결과가 생성되면 학습 작업으로 이어지도록 설정할 수 있습니다.
Data Ready
│
▼
Collection Complete
│
▼
Start Processing
│
▼
Processing Complete
│
▼
Start Training

작업 조건을 연결하면 각 단계의 처리 상태에 따라 데이터 수집부터 AI 처리와 모델 생성까지 순서에 맞춰 실행할 수 있습니다.
결과 전송
생성된 모델과 처리 결과를 필요한 장비로 전송하기
학습과 처리 작업이 완료되면 생성된 모델 파일과 결과 데이터를 다음 환경으로 전송합니다.
모델 저장소, 추론 장비, 검증 환경, 업무 시스템 등 파일을 활용할 위치를 연결하고, 각 결과물을 필요한 대상에 맞춰 전송할 수 있습니다.
AI Training
│
▼
Model Generated
│
┌───┴───────────┐
▼ ▼
Model Storage Inference Server
│
▼
Result Output
│
▼
Target System
하나의 모델 또는 결과 파일을 여러 환경에서 활용하는 경우에는 대상별 파일 흐름을 연결해 필요한 작업 위치까지 자동으로 확장할 수 있습니다.
실행 관리
데이터부터 모델과 결과까지 단계별 처리 상태 확인하기
실행된 워크플로는 전체 흐름과 각 단계의 작업 상태를 함께 확인합니다.
데이터 수집과 전처리, AI 처리, 모델 생성, 결과 전송 과정에서 처리된 파일과 진행 상태를 단계별로 확인할 수 있습니다.
AI Workflow
│
├── Data Collection ✓
│
├── Processing ✓
│
├── Model Training ●
│
└── Result Transfer ○

주요 확인 항목은 다음과 같습니다.
| 확인 항목 | 확인 내용 |
|---|---|
| 단계 | 현재 실행 중인 워크플로 단계 |
| Source | 데이터를 수집한 장비와 파일 위치 |
| Files | 처리된 파일 수와 전체 용량 |
| Progress | 단계별 처리 진행 상태 |
| Target | 모델과 결과 파일의 전송 대상 |
| Status | 전체 워크플로와 각 작업의 실행 결과 |
이를 통해 특정 단계의 처리 현황과 전체 AI 파일 흐름을 함께 관리할 수 있습니다.
예외 대응
전송과 처리 상태를 확인하고 필요한 단계 다시 실행하기
실행 과정에서 추가 확인이 필요한 단계가 있으면 해당 작업의 상세 정보와 실행 기록을 기준으로 데이터 상태와 장비 연결, 파일 전송 결과를 확인합니다.
데이터 수집부터 AI 처리와 결과 전송까지 워크플로의 각 단계를 구분해 확인한 후 필요한 작업을 다시 실행할 수 있습니다.
Workflow Run
│
▼
Stage Status
│
┌────┴─────┐
▼ ▼
Completed Check Required
│
▼
Run Details
│
┌────────┼────────┐
▼ ▼ ▼
Data Device Transfer
│
▼
Retry Stage
│
▼
Result Check

개발자
데이터셋을 여러 학습 노드로 배포하고 노드별 결과를 보관 장비로 회수하기
연동 준비
공통 호출 코드와 경로 표기 준비하기
import os
import requests
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID") # optional; falls back to the current workspace
STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99} # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}
def api(method, path, body=None, params=None):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}",
}
if WORKSPACE_ID:
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(
method, BASE_URL + path,
headers=headers, json=body, params=params, timeout=30,
)
payload = response.json() if response.content else {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def is_terminal(detail):
return detail.get("isTerminal", detail.get("status") in TERMINAL)import base64
import time
def encode_path(device_id, raw_path):
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())전송 상태는 아래 값으로 판단합니다. 종료 상태는 다섯 개이고 성공에 해당하는 값은 완료(2)입니다.
| 상태 값 | 의미 | 종료 |
|---|---|---|
| 2 | 완료 | 예 |
| 4 | 오류 | 예 |
| 5 | 취소 | 예 |
| 9 | 부분 완료 | 예 |
| 99 | 실패 | 예 |
| 1 · 6 · 12 · 13 | 시작·전송 중·동기화 중·수신 중 | 아니오 |
경로 규칙
데이터셋 버전과 실행 식별자를 경로에 담기
회차가 쌓이면 어느 결과가 어느 데이터로 나온 것인지 알 수 없게 됩니다. 경로 규칙을 함수로 고정해 두면 코드 곳곳에서 문자열을 조립하지 않아도 됩니다.
DATA_ROOT = "/data"
WORK_ROOT = "/work"
ARCHIVE_ROOT = "/archive"
# source dataset on the storage device
def dataset_path(dataset, version):
return f"{DATA_ROOT}/datasets/{dataset}/{version}"
# path the training job reads on the node
def node_input_path(dataset, version, run_id):
return f"{WORK_ROOT}/{run_id}/input/{dataset}/{version}"
# path the node writes its results to
def node_output_path(run_id):
return f"{WORK_ROOT}/{run_id}/output"
# archive path where results are collected
def archive_path(run_id):
return f"{ARCHIVE_ROOT}/runs/{run_id}"
run_id = f"r-{time.strftime('%Y%m%d-%H%M%S')}"실행 식별자가 경로 맨 앞에 있으면 회차 단위로 삭제하거나 이동하기 쉽습니다. 날짜만 쓰면 같은 날 두 번 실행했을 때 섞이고, 데이터셋 이름만 쓰면 버전을 올렸을 때 이전 결과와 구분되지 않습니다.
데이터 배포
데이터셋을 여러 노드로 보내고 전원 도착 확인하기
하나의 전송은 대상 하나로 갑니다. 노드가 여럿이면 전송도 여럿입니다.
def deploy(storage, nodes, dataset, version, run_id):
source_path = dataset_path(dataset, version)
target_path = node_input_path(dataset, version, run_id)
return {
node: api("POST", "/api/transfers/manual", {
"sourceDevice": storage,
"targetDevice": node,
"targetPath": target_path,
"sourcePaths": [source_path],
"sendAllFolder": True,
"checkIntegrity": True,
"transferOptions": {"target-action": "overwrite"},
})["monitorId"]
for node in nodes
}def wait(monitor_id, timeout=3600, interval=3):
deadline = time.time() + timeout
while time.time() < deadline:
detail = api("GET", f"/api/transfers/{monitor_id}")
if is_terminal(detail):
return detail
time.sleep(interval)
raise TimeoutError(monitor_id)
def failed_files(monitor_id):
result = api("GET", f"/api/transfers/{monitor_id}/files", params={
"state": "any", "size": 500,
}) or {}
return [r for r in (result.get("children") or [])
if r.get("status") in NOT_SUCCEEDED]
def retry_failed(monitor_id):
rows = failed_files(monitor_id)
if not rows:
return 0
api("POST", f"/api/transfers/{monitor_id}/retry", {
"filesRetry": [
{"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
for r in rows
]
})
return len(rows)한 노드라도 못 받았으면 학습을 시작하면 안 됩니다. 첫 실패에서 멈추지 말고 모든 노드를 확인해야 재시도 범위를 정할 수 있습니다.
transfers = deploy(STORAGE, NODES, "imagenet", "v3", run_id)
results = {node: wait(mid) for node, mid in transfers.items()}
incomplete = [n for n, d in results.items() if d["status"] != STATUS_COMPLETE]
if incomplete:
raise RuntimeError(f"nodes not fully delivered: {', '.join(incomplete)}")도착 정책은 overwrite를 씁니다. numbering을 쓰면 노드에 사본이 쌓이고 학습 코드가 어느 것을 읽을지 알 수 없게 됩니다.
변경분 전송
마지막 배포 이후 바뀐 데이터만 보내기
monitor_id = api("POST", "/api/transfers/manual", {
"sourceDevice": STORAGE,
"targetDevice": node,
"targetPath": node_input_path("imagenet", "v3", run_id),
"sourcePaths": [dataset_path("imagenet", "v3")],
"sendAllFolder": True,
"incremental": True,
"transferOptions": {"target-action": "overwrite"},
})["monitorId"]
detail = wait(monitor_id)
print(f"transferred {detail['fileCount']} files, {detail['totalSize']} bytes")기본값은 꺼짐입니다. 변경분 계산은 노드의 에이전트가 수행하며, 마지막 배포 이후 추가되거나 수정된 파일만 전송합니다.
증분에는 반드시 overwrite를 씁니다. numbering이면 바뀐 파일이 번호가 붙어 쌓이고 학습 코드는 예전 파일을 계속 읽게 됩니다.
무결성 확인
전송된 데이터가 원본과 같은지 확인하기
무결성 검증은 두 단계로 나뉩니다. POST /api/transfers/{monitorId}/verification으로 검증을 시작하고, 서버가 처리하는 동안 GET으로 결과를 폴링합니다.
def verify(monitor_id, timeout=1800, interval=10):
api("POST", f"/api/transfers/{monitor_id}/verification", {})
deadline = time.time() + timeout
while time.time() < deadline:
result = api("GET", f"/api/transfers/{monitor_id}/verification") or {}
if result.get("verified"):
return result
time.sleep(interval)
raise TimeoutError(f"verification: {monitor_id}")
result = verify(monitor_id)
if result["sourceFileCount"] != result["targetFileCount"]:
raise RuntimeError("file counts differ - part of the transfer is missing")
if not result["checksumMatched"]:
for row in result.get("mismatchedFiles") or []:
print("mismatch:", row)| 응답 항목 | 내용 |
|---|---|
checksumAlgorithm | 사용된 체크섬 알고리즘 |
sourceFileCount · targetFileCount | 소스와 대상의 파일 수 |
checksumMatched | 체크섬 일치 여부 |
mismatchedCount · mismatchedFiles | 불일치 건수와 목록 |
파일 수가 다르면 전송이 덜 된 것이고, 수는 같은데 불일치가 있으면 내용이 깨진 것입니다. 전자는 재전송으로 해결되지만 후자는 원인을 찾아야 합니다.
결과 수집
노드별 출력을 겹치지 않게 보관 장비로 모으기
모든 노드가 model.pt처럼 같은 이름을 씁니다. 한 경로로 모으면 서로 덮어씁니다.
def collect(nodes, archive, run_id):
transfers = {}
for node in nodes:
target_path = f"{archive_path(run_id)}/{node}"
transfers[node] = api("POST", "/api/transfers/manual", {
"sourceDevice": node,
"targetDevice": archive,
"targetPath": target_path,
"sourcePaths": [node_output_path(run_id)],
"sendAllFolder": True,
"checkIntegrity": True,
"transferOptions": {"target-action": "overwrite"},
})["monitorId"]
return transfers/archive/runs/r-20260901-0200/
dev-gpu-01/
model.pt
metrics.json
dev-gpu-02/
model.pt
metrics.json
도착 경로에 노드 식별자를 넣는 것이 가장 단순합니다. numbering으로 충돌을 피하면 model_1.pt가 어느 노드 것인지 알 수 없습니다.
모델 파일과 함께 지표와 로그도 수집합니다. 실패한 학습의 원인은 로그에 남는데, 노드가 회수되면 함께 삭제됩니다.
배포와 수집 연결
배포가 끝나면 수집이 자동으로 시작되게 하기
앞의 코드는 클라이언트가 완료를 기다렸다 다음을 호출합니다. 학습이 몇 시간 걸리면 그동안 프로세스가 살아 있어야 하고, 죽으면 수집이 일어나지 않습니다.
import uuid
flow_id = str(uuid.uuid4())
def build_step(name, source, source_path, target, target_path,
step, trigger_id=None, webhook=None):
schedule = {
"type": "none",
"startDateType": "now",
"startDate": now_iso(),
"timezone": "Asia/Seoul",
}
if trigger_id:
schedule["triggerAutomation"] = {"value": trigger_id}
body = {
"name": name,
"flowName": name,
"flowId": flow_id,
"transferType": "normal",
"timezone": "Asia/Seoul",
"step": step,
"isUpcoming": False,
"details": [
{
"senderId": source,
"receiverId": target,
"sourceItem": [
{
"hash": encode_path(source, source_path),
"filePath": source_path,
"isDir": True,
}
],
"targetPath": encode_path(target, target_path),
"step": step,
"transferOptions": {
"noSchedule": False,
"target-action": "overwrite",
"send-fileoption": {},
},
}
],
"schedules": [schedule],
}
if webhook:
body["processors"] = [{
"category": "run",
"type": "http",
"config": {"url": webhook, "method": "POST"},
}]
return body
deploy_id = api("POST", "/api/automations", build_step(
f"deploy imagenet:v3", STORAGE, dataset_path("imagenet", "v3"),
node, node_input_path("imagenet", "v3", run_id),
step=1, webhook=TRAIN_HOOK))["automationId"]
collect_id = api("POST", "/api/automations", build_step(
f"collect {run_id}", node, node_output_path(run_id),
ARCHIVE, f"{archive_path(run_id)}/{node}",
step=2, trigger_id=deploy_id))["automationId"]자동화 요청에서 반드시 지켜야 하는 항목이 네 개 있습니다.
| 항목 | 지정 방식 |
|---|---|
isUpcoming | 반드시 false. 서버 기본값 true는 요청에 담긴 일정을 무시하고 5분짜리 일회성 일정으로 대체합니다. triggerAutomation이 붙은 단계는 서버가 false로 강제하므로, 트리거가 없는 첫 단계에만 직접 지정하면 됩니다 |
step | 최상위와 details 양쪽에 넣습니다. 흐름 안에서의 홉 위치입니다 |
sourceItem | hash(경로 토큰)와 filePath(평문 경로)를 함께 넣습니다 |
syncType | transferOptions 안에 넣습니다. 1은 단방향, 2는 양방향입니다 |
네 항목 모두 누락해도 등록은 성공하고 실행 시점에 동작이 달라집니다. 반복 일정을 등록했는데 한 번만 실행되고 끝났다면 isUpcoming부터 확인합니다.
두 요청을 보내고 나면 애플리케이션의 일은 끝납니다. 단계별 식별자는 실행 기록에 함께 저장해 둬야 나중에 상태를 확인할 수 있습니다.
학습 호출과 재전송
데이터 도착 시점에 학습을 호출하고 실패한 파일만 재전송하기
호출은 전송 완료 후에 오며, 그 요청을 받는 엔드포인트는 다음과 같이 처리합니다.
def on_train_hook(payload):
monitor_id = payload.get("monitorId")
if monitor_id:
detail = wait(monitor_id)
if detail["status"] != STATUS_COMPLETE:
return abort_run(payload)
start_training(payload)회차별 결과는 실행 이력으로 확인합니다.
runs = api("GET", f"/api/automations/{deploy_id}/executions") or []
latest = runs[0] if runs else None
if latest and latest["status"] != STATUS_COMPLETE:
print("deploy failed:", latest["monitorId"],
"retried", retry_failed(latest["monitorId"]), "files")데이터셋이 크면 몇 개 파일 때문에 전체를 재전송하는 것이 큰 손실입니다.
| 확인 항목 | 확인 내용 |
|---|---|
| 경로 | 데이터셋 버전과 실행 식별자 |
| 배포 | 노드별 전달 완료 여부 |
| 검증 | 파일 수와 체크섬 일치 여부 |
| 수집 | 노드별 결과 도착 경로 |
| 흐름 | 단계별 회차와 상태 |
| 재전송 | 실패한 파일과 처리 결과 |