히스토리
- 업데이트
HomeAgent ML 파이프라인 도구세트 운영전략
NixOS GPU 클러스터에서 ML 파인튜닝 파이프라인을 운영하기 위한 도구세트 결정 과정. 알고리즘이 아닌 운영 인프라 관점 — 도구 선택, 에이전트 협업, 상태 모니터링에 집중한다.
배경: 왜 이 파이프라인이 필요한가
HomeAgent는 엣지 NPU(Hailo-8/10H)에서 동작하는 Constitutional AI 에이전트다. 클라우드 API 호출 없이 오프라인에서 자율 판단해야 하므로, 로컬 GPU 클러스터에서 파인튜닝 → 양자화 → 엣지 배포하는 파이프라인이 필요하다.
파인튜닝 파이프라인 (gpu-03, 6단계 ~1시간):
데이터 준비 → QLoRA 학습 → LoRA 병합 → 평가 → ONNX 변환 → HEF 양자화
(CPU ~5분) (GPU ~30min) (GPU ~5분) (GPU ~10분) (CPU/GPU) (CPU ~10분)
│
↓ SCP / NFS
RPi5 + Hailo-8 추론Phase 1: Qwen3-0.6B × 2개 (Intent + Summary), Hailo-8 Phase 2: Qwen3-1.5B 통합 모델, Hailo-10H
현재 인프라 상태 (2026-02-09)
| 구성요소 | 상태 | 위치 | 비고 |
|---|---|---|---|
| 파인튜닝 Python 환경 | tuning.nix 완료 (17패키지) | gpu-03 | NixOS 25.11 네이티브 |
| QLoRA 검증 | test-qlora.py 성공 | gpu-03 | Qwen3-0.6B, ~4GB VRAM |
| wandb | 설치됨 (미연결) | gpu-03 tuning.nix | report_to=“none” 상태 |
| Temporal Server 1.29.3 | 가동중 (Docker) | storage-01:7233 | helloworld 검증 완료 |
| Temporal UI 2.45.0 | 가동중 (Docker) | storage-01:8233 | 웹 모니터링 |
| Doltgres 0.54.10 | 가동중 (Docker) | storage-01:5434 | Git-like 버전관리 PostgreSQL |
| NFS storage/models | 마운트됨 (10G) | storage-01 → gpu-03 | 모델 아티팩트 |
| 이슈 트래킹 | br (JSONL + git) | 로컬 | 에이전트 협업용 |
핵심: 도구는 이미 가동중인데 배선이 안 된 상태. Temporal과 Doltgres는 원래 Tuya IoT 알림톡 브릿지용으로 배포됐지만, ML 파이프라인에도 재활용할 수 있다.
풀어야 할 3가지 질문
- 학습 실험 추적 — wandb (SaaS) vs MLflow (self-host)
- 파이프라인 오케스트레이션 — Temporal vs 수동 스크립트
- 데이터셋/모델 버전 관리 — Doltgres vs DVC vs git-lfs
Q1 확정: 실험 트래킹 → wandb SaaS
문제
파인튜닝을 반복하면 하이퍼파라미터, loss 곡선, GPU 사용률을 비교해야 한다. “v3가 왜 v2보다 나은가?”를 나중에 재현할 수 있어야 한다.
비교
| 기준 | wandb SaaS | MLflow (NixOS 셀프호스팅) |
|---|---|---|
| 설정 비용 | report_to”wandb”= 1줄 | 서비스 구성, 포트, 스토리지 |
| HF transformers 통합 | 네이티브 | report_to”mlflow”= |
| 대시보드 | wandb.ai 즉시 사용 | http://gpu-03:5000 구축 필요 |
| 데이터 프라이버시 | 클라우드 의존 | 완전 자체 호스팅 |
| NixOS 패키지 | tuning.nix에 이미 있음 | mlflow-server 있으나 버그 보고 (GH#330746) |
| 비용 | 무료 (퍼블릭) | 무료 |
결정: wandb SaaS로 시작
근거:
- tuning.nix에 이미 설치되어 있고, 코드 1줄 변경으로 즉시 활성화
- Phase 1 데이터(홈 Intent JSON)는 민감하지 않음
- 노트북에서 wandb.ai 접속만으로 실시간 대시보드
전환 계획:
- Phase 2 Constitutional 데이터가 민감해지면 → MLflow 셀프호스팅 또는 wandb 셀프호스팅
- MLflow는 nixpkgs에 패키지가 있으므로 전환 부담 낮음
적용 방법:
# test-qlora.py → train-intent.py 변경점
training_args = TrainingArguments(
report_to="wandb", # "none" → "wandb"
run_name="intent-v1-qlora-r16",
)# 최초 1회
ssh gpu-03 "wandb login"Q2 확정: 파이프라인 오케스트레이션 → Temporal Python Worker
문제
6단계 선형 파이프라인을 어떻게 실행하고, 실패를 관리하고, 진행상황을 모니터링하는가. 특히 AI 에이전트(Claude)가 학습을 트리거하고 결과를 확인할 수 있어야 한다.
비교
| 기준 | 수동 스크립트 | Temporal |
|---|---|---|
| 설정 비용 | 0 (ssh + python) | Worker 배포 필요 |
| 장애 복구 | 처음부터 재실행 (~1시간) | 중단점 복구 (~10분) |
| 모니터링 | stdout + wandb | Temporal UI + wandb |
| 에이전트 협업 | ssh 명령 직접 실행 | API로 워크플로우 트리거/조회 |
| 확장성 | 단일 스크립트 한계 | 멀티노드, 분기, 스케줄링 |
| 서버 인프라 | 불필요 | 이미 storage-01에 가동중 |
조사 결과: Temporal Python SDK
- v1.22.0 (2026-02-03), GA, 프로덕션 검증 완료
- OpenAI, Descript 등에서 ML 파이프라인에 실사용 중
- Python 3.10-3.14 지원, async/threaded/multiprocess Activity 모두 지원
- GPU Task Queue: gpu-03에 Python Worker를 띄우고, GPU Activity를 전용 큐로 라우팅
- Heartbeat: 장시간 학습(30min+) 중 진행상황 보고 → Worker 죽으면 감지 후 재시도
결정: Temporal Python Worker
근거:
- Temporal Server가 이미 storage-01에서 가동중 — Worker만 추가하면 됨
- “순수 스크립트 + 실패 시 처음부터”보다 “Activity 단위 재시도”가 실용적
- Temporal UI(8233)에서 파이프라인 진행상황을 노트북에서 바로 확인 가능
- Claude가 Temporal API로 워크플로우 트리거/상태조회 가능 → 에이전트 협업의 핵심
- Python SDK가 GA이므로 tuning.nix 환경과 자연스럽게 통합
아키텍처:
┌─ storage-01 ─────────────────────────────────────┐
│ Temporal Server (Docker) │
│ └─ gRPC: 192.168.165.141:7233 │
│ └─ UI: http://192.168.165.141:8233 │
│ Doltgres (Docker) │
│ └─ PostgreSQL: 192.168.165.141:5434 │
└──────────┬────────────────────────────────────────┘
│ gRPC (Worker가 Server를 폴링)
↓
┌─ gpu-03 ─────────────────────────────────────────┐
│ Temporal Python Worker │
│ └─ TaskQueue: "gpu-training" │
│ └─ Activity: prepare_dataset (CPU) │
│ └─ Activity: train_qlora (GPU, heartbeat) │
│ └─ Activity: merge_lora (GPU) │
│ └─ Activity: evaluate (GPU) │
│ └─ Activity: export_onnx (CPU/GPU) │
│ └─ Activity: quantize_hef (CPU) │
└──────────────────────────────────────────────────┘Workflow 예시 (Python):
@workflow.defn
class QLoRAPipeline:
@workflow.run
async def run(self, config: PipelineConfig) -> PipelineResult:
# 1. Doltgres에서 데이터셋 로드
dataset = await workflow.execute_activity(
prepare_dataset, config,
start_to_close_timeout=timedelta(minutes=10),
)
# 2. QLoRA 학습 (장시간, heartbeat 필수)
model = await workflow.execute_activity(
train_qlora, config,
task_queue="gpu-training",
start_to_close_timeout=timedelta(hours=1),
heartbeat_timeout=timedelta(minutes=5),
retry_policy=RetryPolicy(maximum_attempts=3),
)
# 3~6. 후속 단계...
return PipelineResult(metrics=metrics, artifact_path=path)Heartbeat 패턴 (30분 학습):
@activity.defn
async def train_qlora(config: QLoRAConfig) -> TrainResult:
trainer = setup_trainer(config)
for epoch in range(config.num_epochs):
for step, batch in enumerate(trainer.dataloader):
loss = trainer.train_step(batch)
if step % 100 == 0:
activity.heartbeat(f"epoch={epoch}, step={step}, loss={loss:.4f}")
return TrainResult(model_path=trainer.save())Q3 확정: 데이터셋 버전 관리 → Doltgres + NFS
문제
학습 데이터셋의 변경 이력을 추적하고, 실험 간 “어떤 샘플이 바뀌었는지”를 알아야 한다. 모델 바이너리(1-2GB)는 별도 관리가 필요하다.
아티팩트별 관리 전략
| 아티팩트 | 크기 | 관리 도구 | 근거 |
|---|---|---|---|
| Intent 데이터셋 | < 5MB (500+ 행) | Doltgres | SQL 쿼리 + row-level diff |
| Summary 데이터셋 | < 5MB (300+ 행) | Doltgres | 동일 |
| LoRA 어댑터 | ~50MB | NFS | 재생성 가능 |
| 병합 모델 (FP16) | ~1.2GB | NFS | 재생성 가능 |
| ONNX 모델 | ~1.5GB | NFS | 재생성 가능 |
| HEF 파일 | ~수 MB | homeagent-config git | 최종 산출물 |
비교: Doltgres vs DVC vs git-lfs
| 기준 | Doltgres | DVC | git-lfs |
|---|---|---|---|
| 데이터 모델 | SQL 테이블 (관계형) | 파일 (모든 형식) | 파일 (모든 형식) |
| Diff 단위 | 행 단위 (row-level) | 파일 단위 | 파일 단위 |
| 브랜칭 | SQL로 브랜치/머지 | Git 브랜치 활용 | Git 브랜치 활용 |
| 쿼리 | 완전한 SQL | 불가 | 불가 |
| 비정형 데이터 | 부적합 | 적합 | 적합 |
| 생태계 | 독자적 (성장중) | lakeFS에 인수됨 (2025-11) | GitHub 네이티브 |
| 현재 상태 | 이미 가동중 | 별도 설치 필요 | 별도 설치 필요 |
조사 결과: Doltgres
- v0.54.10, Beta (1.0 목표 2026 Q2), PostgreSQL 호환
- Prolly Tree 스토리지: Git의 Merkle Tree와 유사한 구조로 효율적 diff/merge
- Python 통합:
psycopg2,SQLAlchemy등 표준 PostgreSQL 드라이버 그대로 사용 - Flock Safety 사례: Dolt를 ML Feature Store로 프로덕션 사용 (버전 관리 + 브랜치 격리)
- DVC 대비 핵심 장점: 행 단위 diff, SQL 쿼리, 시간 여행(time-travel) 쿼리
주의사항:
- CLI 없음 — 모든 버전 관리 조작이 SQL 함수를 통해야 함
- 파일로 저장하지 않음 — 학습 프레임워크가 JSONL을 기대하면 변환 필요
- 500-1000행 규모에서는 변환 오버헤드 무시 가능
결정: Doltgres (데이터셋) + NFS (아티팩트)
근거:
- storage-01에 이미 가동중 — 추가 인프라 불필요
- 홈 Intent/Summary 데이터셋은 구조화된 JSON → SQL 테이블로 자연스럽게 매핑
dolt_diff_training_samples로 “v1에서 v2로 어떤 샘플이 바뀌었나” 추적 가능- DVC는 lakeFS에 인수되어 생태계 불확실 → 신규 도입에 리스크
Doltgres 테이블 설계:
CREATE TABLE intent_samples (
id SERIAL PRIMARY KEY,
instruction TEXT NOT NULL,
input TEXT,
output TEXT NOT NULL,
category VARCHAR(50),
quality_score FLOAT,
meta JSONB, -- 실험용 유연 필드 (스키마 변경 없이 확장)
created_at TIMESTAMP DEFAULT NOW()
);meta JSONB 컬럼의 역할:
- 핵심 필드(instruction, output)는 고정 컬럼 → row-level diff의 대상
- 실험적 필드(context, difficulty, source 등)는
meta에 → 스키마 변경 없이 빠른 실험 - 예:
INSERT INTO intent_samples (..., meta) VALUES (..., '{"source": "manual", "difficulty": "hard"}')
브랜치/디프/커밋 워크플로우:
-- 실험 브랜치 생성
SELECT dolt_branch('experiment/intent-v2');
SELECT dolt_checkout('experiment/intent-v2');
-- 샘플 추가/수정
INSERT INTO intent_samples (instruction, output) VALUES (...);
-- 커밋
SELECT dolt_add('-A');
SELECT dolt_commit('-m', 'Add 50 new intent samples for edge cases');
-- v1 vs v2 diff (행 단위!)
SELECT diff_type, from_instruction, to_instruction, from_output, to_output
FROM dolt_commit_diff_intent_samples
WHERE from_commit = HASHOF('experiment/intent-v1')
AND to_commit = HASHOF('experiment/intent-v2');
-- 학습 스크립트에서 데이터 로드
-- Python: engine = create_engine("postgresql+psycopg2://...:{port}/ml_datasets/experiment/intent-v2")학습 스크립트에서 Doltgres → JSONL 변환:
import pandas as pd
from sqlalchemy import create_engine
import json
engine = create_engine(
"postgresql+psycopg2://postgres@192.168.165.141:5434/ml_datasets"
)
df = pd.read_sql(
"SELECT instruction, input, output FROM intent_samples WHERE quality_score > 0.8",
engine,
)
# HuggingFace datasets 형식으로 변환
records = df.to_dict(orient='records')확정: 미래 아키텍처
┌─ 노트북 (ThinkPad P16s) ─────────────────────────────────────┐
│ │
│ Temporal UI (8233) → 파이프라인 진행상황 모니터링 │
│ wandb.ai → 학습 메트릭 (loss, GPU, 하이퍼파라미터) │
│ br ready → 이슈 확인 │
│ Claude Code → br + Temporal API 트리거 + 결과 해석 │
│ │
└────────────────────────────────────────────────────────────────┘
│ ssh / https / gRPC
↓
┌─ storage-01 (Docker) ─────────────────────────────────────────┐
│ │
│ Temporal Server 1.29.3 → 파이프라인 오케스트레이션 │
│ └─ gRPC: 192.168.165.141:7233 │
│ └─ UI: http://192.168.165.141:8233 │
│ │
│ Doltgres 0.54.10 → 데이터셋 버전 관리 │
│ └─ PostgreSQL: 192.168.165.141:5434 │
│ └─ branch: main, experiment/intent-v1, v2... │
│ └─ row-level diff: "어떤 샘플이 바뀌었나" │
│ │
│ /storage/models/tuning/ → 모델 아티팩트 (NFS) │
│ │
└───────────────────────────────┬────────────────────────────────┘
│ gRPC (Worker 폴링) + NFS (10G)
↓
┌─ gpu-03 (파인튜닝 전용, RTX 5080 16GB) ──────────────────────┐
│ │
│ tuning.nix: Python 17패키지 (NixOS 25.11 네이티브) │
│ │
│ Temporal Python Worker │
│ └─ TaskQueue: "gpu-training" │
│ └─ Doltgres SQL → JSONL 변환 (Activity 1) │
│ └─ QLoRA 학습 + wandb 기록 (Activity 2, heartbeat) │
│ └─ LoRA 병합 (Activity 3) │
│ └─ 평가 → Doltgres에 결과 기록 (Activity 4) │
│ └─ ONNX 변환 (Activity 5) │
│ └─ HEF 양자화 (Activity 6) │
│ │
│ wandb: report_to="wandb" (학습 메트릭 자동 기록) │
│ │
└────────────────────────────────────────────────────────────────┘
│ SCP / NFS
↓
┌───────────────────────┐
│ RPi5 + Hailo-8 │
│ HEF 추론 검증 │
└───────────────────────┘재현성 공식
재현 = flake.lock (환경)
+ Doltgres commit hash (데이터셋)
+ git commit (코드)
+ wandb run ID (하이퍼파라미터 + 메트릭)이 4가지를 기록하면 “정확히 같은 환경에서, 같은 데이터로, 같은 코드와 설정으로” 학습을 재현할 수 있다. Docker 없이, NixOS 네이티브 + 기존 인프라만으로.
리스크 대응 (Gemini 검토 반영, 2026-02-09)
Gemini의 피어 리뷰에서 지적된 3가지 리스크와 대응 전략. 총평: “이력서 주도 개발이 아니라, 자산 최적화 주도 개발(Asset-Optimization Driven Development)“
리스크 1: 스키마 경직성
문제: JSON 파일은 필드 추가가 자유롭지만, RDBMS는 ALTER TABLE이 필요. 초기 실험에서 데이터셋 구조가 자주 바뀔 수 있다.
대응: 하이브리드 스키마 — 핵심 필드는 고정 컬럼(SQL diff 대상), 실험 필드는 meta JSONB 컬럼. 테이블 설계에 이미 반영 완료.
리스크 2: Beta 소프트웨어 의존
문제: Doltgres 0.54.10은 Beta. 데이터 깨짐, 커넥션 끊김, 락 등의 가능성.
대응: JSONL 탈출구 — Temporal 워크플로우의 마지막 단계 또는 별도 Cron으로 Doltgres 데이터를 NFS에 JSONL 백업.
# 백업 Activity 예시
@activity.defn
async def backup_to_jsonl():
engine = create_engine("postgresql+psycopg2://postgres@192.168.165.141:5434/ml_datasets")
df = pd.read_sql("SELECT * FROM intent_samples", engine)
backup_path = f"/storage/backup/doltgres/intent_samples_{datetime.now():%Y%m%d}.jsonl"
df.to_json(backup_path, orient='records', lines=True, force_ascii=False)핵심: Doltgres가 깨져도 JSONL + git으로 즉시 폴백 가능. 이 폴백 경로가 있기 때문에 Beta 소프트웨어를 시도할 수 있다.
리스크 3: 디버깅 복잡도
문제: NixOS + Python Worker + 원격 DB + 비동기 워크플로우 조합은 디버깅이 어렵고 노트북에서 재현하기 까다롭다.
대응: Local Mode 개발 가이드
[개발 시] 노트북 → storage-01 Temporal Server + Doltgres에 직접 연결
- Worker를 노트북에서 실행 (GPU 없이 CPU 모드로 테스트)
- Temporal Server: 192.168.165.141:7233 (사내망)
- Doltgres: 192.168.165.141:5434 (사내망)
- 소규모 데이터(10행)로 파이프라인 전체 흐름 검증
[배포 시] gpu-03에 Worker 배포 → GPU TaskQueue 폴링
- 실제 학습은 gpu-03에서만 실행Worker가 어디서 실행되든 Temporal Server에 붙기만 하면 되는 구조이므로, 노트북에서 개발 → gpu-03에서 실행의 전환이 단순하다.
미개척 영역에 대한 메모
Temporal + Doltgres 조합은 *2026-02 현재 아무도 공개적으로 시도하지 않은 패턴*이다.
유사 사례:
- Dolt + Metaflow (Netflix 워크플로우 엔진) — 공식 통합
- Dolt + Flyte (Kubernetes ML 오케스트레이터) — 데이터 리니지 통합
- Flock Safety + Hosted Dolt — ML Feature Store로 프로덕션 사용
- Temporal + OpenAI Agents SDK — AI 에이전트 오케스트레이션
우리 조합이 독특한 점:
- NixOS 네이티브 환경에서 재현성 보장
- 소규모 (단일 GPU, 500행 데이터셋) 에서 시작하는 현실적 접근
- 에이전트(Claude) 협업이 설계에 포함됨
- 이미 가동중인 인프라를 재활용
안 되더라도 시도하는 것에 가치가 있다:
- 실패하면 순수 Python 스크립트 + git JSONL로 폴백 가능 (~1일 전환)
- 성공하면 row-level 데이터 리니지 + durable execution을 확보
Phase 전환 가이드
| 조건 | 전환 |
|---|---|
| Phase 2 민감 데이터 | wandb SaaS → MLflow 셀프호스팅 |
| 데이터셋 10만+ 행 | Doltgres 단독 → Doltgres + DVC (비정형) |
| 멀티노드 학습 (gpu-01+02+03) | 단일 Worker → 멀티 Worker (TaskQueue 분리) |
| 모델 배포 자동화 | 수동 SCP → Temporal Workflow (HEF → RPi5 OTA) |
| Doltgres 1.0 출시 (2026 Q2 목표) | Beta 관찰 → 안정화 확인 |
다음 행동 (Action Items)
즉시 (Phase 1 시작):
- wandb login 실행 (gpu-03)
- test-qlora.py → train-intent.py 전환 (report_to=“wandb”)
- Doltgres에 ml_datasets DB + intent_samples 테이블 생성 (meta JSONB 포함)
- 샘플 데이터 10개를 Doltgres에 INSERT
다음 단계 (Temporal Worker):
- temporalio Python 패키지를 tuning.nix에 추가
- 노트북에서 Local Mode로 Worker 개발/테스트 (CPU, 소규모 데이터)
- gpu-03에 Temporal Python Worker 배포 (6 Activity)
- 파이프라인 1회 실행 → Temporal UI + wandb 동시 확인
안전장치:
- Doltgres → JSONL 백업 Activity 구현 (NFS에 저장)
- 백업 Cron 또는 파이프라인 마지막 단계에 통합
검증:
- 학습 완료 후 Doltgres에 평가 결과 기록
- 데이터셋 v2 브랜치 → diff 확인 → 재학습
- 전체 재현성 검증 (flake.lock + Doltgres commit + wandb run + git commit)
- JSONL 백업에서 복원 테스트 (폴백 경로 검증)
참고 자료
프로젝트 문서
- hej-nixos-cluster/MODEL_TUNING.md — 전체 전략서
- hej-nixos-cluster/docs/ml-파이프라인-도구세트-가이드 — 도구 비교 (초안)
- hej-nixos-cluster/modules/gpu/tuning.nix — NixOS 환경 정의 (17패키지)
- hej-nixos-cluster/scripts/test-qlora.py — QLoRA 검증 스크립트
- hej-kip/temporal_bridge/ — Temporal + Doltgres Docker 구성
조사 출처
Temporal:
- temporalio PyPI — v1.22.0, Python SDK
- ML Workflows with Temporal — ML 파이프라인 패턴
- 9 Ways to Use Temporal in AI Workflows
- Temporal Python SDK: Failure Detection — Heartbeat/Timeout 설정
- The Four Types of Activity Timeouts
Doltgres:
- DoltgreSQL GitHub — v0.54.10
- Doltgres Documentation
- Why Doltgres over Dolt? — PostgreSQL 선택 근거
- State of Doltgres (2025-10) — 성숙도 현황
- SQLAlchemy + Doltgres Getting Started
- Flock Safety: Dolt as ML Feature Store — 프로덕션 사례
비교:
Comments