LLM 엔지니어링19장 · MLOps 와 LLMOps, 워크플로 자동화10 / 23← 이전목차다음 →MetaContents

19장. MLOps 와 LLMOps, 워크플로 자동화

워크플로와 DAG

김장하는 날을 떠올려 봅니다. 배추를 절이는 동안 양념을 만듭니다. 둘 다 끝나야 버무리고, 버무려야 통에 담습니다. 담은 뒤에 배추를 절일 수는 없습니다. 이렇게 앞뒤가 정해진 작업 묶음이 워크플로이고, 그 앞뒤 관계를 화살표로 그린 것이 DAG 입니다.

문서 읽기, 검사, 조각 나누기, 임베딩, 교체의 다섯 작업을 화살표로 이은 흐름도

그림은 이 장의 색인 갱신 작업입니다. 화살표는 "앞 작업이 끝나야 뒤 작업을 한다"는 뜻입니다. DAG 의 A 는 방향(화살표), C 는 순환이 없다(되돌아오는 길이 없다)는 뜻입니다. 김장에서 배추 절이기와 양념 만들기처럼 서로 기다리지 않는 작업은 함께 돌릴 수 있습니다.

워크플로를 이루는 것

것뜻예
작업(task)한 가지 일문서 읽기
의존먼저 끝나야 하는 작업검사는 문서 읽기 뒤
다시 시도실패하면 몇 번 더 해 볼지공유 폴더가 잠깐 끊겨도 한 번 더
일정언제 돌릴지매일 새벽 2시
기록작업마다 성공, 실패, 건너뜀운영 기록표

작업 순서 정하기

파이썬의 graphlib 은 앞뒤 관계를 받아 돌릴 차례를 정해 줍니다. 순환이 있으면 오류를 냅니다.

Python
from graphlib import CycleError, TopologicalSorter

# 작업: [먼저 끝나야 하는 작업들]
kimchi = {"배추 절이기": [], "양념 만들기": [], "버무리기": ["배추 절이기", "양념 만들기"], "통에 담기": ["버무리기"]}
# 결과: 앞 작업이 늘 먼저 나오는 차례
print(list(TopologicalSorter(kimchi).static_order()))

# 담기가 끝나야 절이기를 한다고 잘못 적으면 순환이 생긴다
try:
    list(TopologicalSorter(dict(kimchi, **{"배추 절이기": ["통에 담기"]})).static_order())
except CycleError as e:
    print("순환:", e.args[1])

코드에 나오는 함수

코드쉬운 뜻
graphlibgraph(그래프, 점과 선으로 된 짜임)를 다루는 파이썬 표준 모듈이다. 따로 설치하지 않는다
TopologicalSorter(kimchi)topological sort(위상 정렬)를 해 주는 도구를 만든다. 위상 정렬은 앞뒤 관계를 지키며 한 줄로 세우는 일이다
.static_order()돌릴 차례를 한꺼번에 정해 내준다. list(…) 로 감싸면 목록이 된다
dict(kimchi, **{"배추 절이기": ["통에 담기"]})kimchi 를 복사하면서 "배추 절이기"의 앞 작업만 바꾼다. 열쇠에 띄어쓰기가 있어 이름=값 꼴로 쓸 수 없으므로 **{…} 꼴로 넘긴다
CycleError순환(cycle)이 있을 때 나는 오류다
e.args[1]오류에 담긴 두 번째 값으로, 순환을 이루는 작업들의 목록이다

이 코드가 뜻하는 것

학교의 선수 과목을 떠올려 봅니다. "수학 II 는 수학 I 을 들은 뒤에"처럼 먼저 들어야 할 과목이 정해져 있으면, 그것을 지키는 수강 순서표를 짤 수 있습니다. "A 는 B 뒤에, B 는 A 뒤에"처럼 서로가 서로를 기다리면 순서표를 짤 수 없습니다. 여기서 과목이 작업, 선수 과목이 의존, 수강 순서표가 static_order(), 서로 기다리는 과목이 순환입니다.

줄하는 일쉽게 말하면
kimchi = {…}작업마다 먼저 끝나야 할 작업을 적는다과목마다 선수 과목을 적는다
list(TopologicalSorter(kimchi).static_order())앞뒤를 지킨 차례를 낸다수강 순서표를 받는다
dict(kimchi, **{…})일부러 순환을 만든다서로가 서로의 선수 과목이 되게 적는다
except CycleError as e순환 오류를 받아 순환을 이루는 길을 보여 준다"이 순서표는 짤 수 없다"며 꼬인 고리를 알려 준다

둘째 출력 ['배추 절이기', '버무리기', '통에 담기', '배추 절이기'] 는 처음과 끝이 같은 고리입니다. 이 고리를 따라가면 영원히 시작할 수 없습니다. 비유와 다른 점은, 학교에서는 한 학기에 여러 과목을 함께 듣지만 static_order() 는 함께 할 수 있는 작업도 한 줄로 늘어놓는다는 것입니다. 함께 할 수 있는 작업은 아래에서 봅니다.

작게 줄여서 눈으로 보기

get_ready() 를 쓰면 "지금 함께 돌릴 수 있는 작업"을 한 묶음씩 받을 수 있습니다.

Python
from graphlib import TopologicalSorter

kimchi = {"배추 절이기": [], "양념 만들기": [], "버무리기": ["배추 절이기", "양념 만들기"], "통에 담기": ["버무리기"]}
ts = TopologicalSorter(kimchi)
ts.prepare()                                  # 순환이 없는지 확인하고 시작한다
round_no = 1
while ts.is_active():
    ready = ts.get_ready()                    # 지금 함께 돌릴 수 있는 작업들
    print(round_no, "차례:", list(ready))
    ts.done(*ready)                           # 끝났다고 알린다
    round_no += 1
출력
1 차례: ['배추 절이기', '양념 만들기']
2 차례: ['버무리기']
3 차례: ['통에 담기']
결과읽는 법
1 차례: ['배추 절이기', '양념 만들기']앞 작업이 없는 둘은 서로 기다리지 않으므로 함께 돌릴 수 있다
2 차례: ['버무리기']앞의 둘이 모두 끝났다고 알린 뒤에야 나온다
3 차례: ['통에 담기']마지막 작업이다. 이 뒤에는 is_active() 가 거짓이 되어 반복이 끝난다

prepare() 는 준비, get_ready() 는 "준비된 것 받기", done() 은 "끝났다고 알리기", is_active() 는 "아직 남은 작업이 있나"라는 뜻입니다. 워크플로 도구의 스케줄러가 작업을 나눠 맡길 때 하는 일이 이것과 닮았습니다.

주의할 점

  • 작업 하나는 한 가지 일만 합니다. 작게 나눠야 실패한 곳만 다시 돌릴 수 있습니다.
  • 앞 작업이 실패하면 뒤 작업은 돌리지 않고 건너뜁니다. 망가진 결과 위에 쌓지 않기 위해서입니다.
← 이전 절19장 · Docker 심화: 모델과 데이터는 밖에 둔다다음 절 →19장 · Airflow 로 일정 돌리기