계산 그래프

편집 역사 토론
소프트웨어 수치해석 시뮬레이션 마지막 수정: 2026-09-14 04:14:22

1. 개요[편집]

계산 그래프
Computational Graph
정체계산을 표현한 유향 비순환 그래프(DAG)
정점 / 간선기본 연산 / 데이터 의존
실행 순서위상 정렬이 허용하는 아무 순서
전진 모드위상 순서대로 훑기 — 그래프를 안 남겨도 된다
역방향 모드역위상 순서로 훑기 — 그래프를 물질화해야 한다
두 진영정적(define-and-run) vs 동적(define-by-run)
최적화상수 접기 · 공통부분식 제거 · 죽은 코드 제거 · 연산 융합
기원벵거트 목록(1964) · 바우어의 계산 그래프(1974)

계산 그래프(computational graph)는 하나의 계산을 정점이 기본 연산, 간선이 데이터 의존을 뜻하는 유향 비순환 그래프로 표현한 자료구조다. y = (a*b) + sin(a) 라는 한 줄은 곱셈 노드 하나, 사인 노드 하나, 덧셈 노드 하나와 그 사이를 잇는 간선 넷이 되고, 입력 a 에서 출력 y 로 가는 경로가 두 갈래 생긴다. 이게 전부다.

이 시시해 보이는 표현이 왜 한 편의 문서를 받아야 하냐면, 자동 미분과 텐서 컴파일러가 둘 다 이 그래프 위에서 산다는 사실 때문이다. 미분은 그래프를 한 방향으로 훑는 일이고, 최적화는 그래프를 다른 그래프로 바꿔치는 일이다. 프로그램을 텍스트나 추상 구문 트리가 아니라 DAG로 다시 본 순간 둘 다 그래프 알고리즘 문제로 바뀌며, 그래서 오늘날 심층 학습 프레임워크와 미분 가능 시뮬레이션 인프라의 밑바닥은 예외 없이 그래프 엔진이다.

미분의 수학 자체는 자동 미분·역전파 문서가 다룬다. 여기서는 그 미분이 올라타는 자료구조, 그리고 그 자료구조를 언제 만들고 언제 없애느냐는 공학적 선택을 다룬다.

2. 그래프에 정확히 무엇이 담기는가[편집]

주의할 것은, 계산 그래프는 소스 코드의 그림이 아니라 실제로 실행된 연산의 기록이라는 점이다. 이 구별이 뒤에 나오는 거의 모든 함정의 뿌리다.

가장 오래된 형태는 벵거트 목록(Wengert list)이다.1 프로그램의 모든 중간값에 이름을 붙여 기본 연산 하나씩만 하는 대입문의 나열로 펼친 것으로, 앞의 예는 이렇게 된다.

v1=a,v2=b,v3=v1v2,v4=sinv1,v5=v3+v4v_1 = a,\quad v_2 = b,\quad v_3 = v_1 v_2,\quad v_4 = \sin v_1,\quad v_5 = v_3 + v_4

이 선형 목록에 “누가 누구를 먹었는가”를 간선으로 그리면 그대로 DAG다. 비순환인 이유도 여기서 나온다 — 각 중간값은 딱 한 번 정의되고, 정의되기 전에는 쓰일 수 없다. 순환 신경망처럼 반복이 있는 계산은 그래프에 고리를 만드는 게 아니라 시간축으로 펼쳐서(unrolling) 여전히 DAG로 만든다. BPTT(시간 역전파)가 그냥 역전파인 이유다.

간선 하나마다 국소 도함수 vj/vi\partial v_j / \partial v_i 가 붙어 있다고 보면 미분은 경로 위의 곱과 경로들 사이의 합으로 환원된다. 정점을 실행하는 순서는 위상 정렬이 허용하는 범위 안에서 자유롭고, 이 자유도가 곧 병렬 실행·메모리 재사용·스케줄링의 여지다.

3. 같은 그래프, 두 방향의 순회[편집]

전진 모드와 역방향 모드의 차이는 알고리즘이 다른 게 아니라 이 그래프를 훑는 방향이 다른 것이다.

  • 전진 모드 — 위상 순서대로 노드를 방문하며 값과 방향도함수를 같이 나른다. 방문이 끝난 노드는 더 볼 일이 없으므로 그래프를 저장할 필요가 없다. 메모리는 원래 함수 평가와 같은 수준이고, 그래프는 사실상 존재하지 않아도 된다.
  • 역방향 모드 — 먼저 위상 순서로 전부 계산해 값을 남기고, 그 다음 역위상 순서로 거슬러 올라가며 수반값을 누적한다. 거슬러 올라가려면 간선이 어디로 연결됐는지와 국소 도함수에 필요한 중간값이 남아 있어야 한다. 즉 그래프를 실제로 메모리에 물질화해야 한다.

이 비대칭이 실무에서 갖는 무게는 절대적이다. 입력이 수억 개(파라미터)이고 출력이 스칼라 하나(손실)인 문제에서는 역방향이 압도적으로 싸고 — 바우어·슈트라센이 보인 대로 전체 기울기를 함수 평가 비용의 상수배(연산 모형에 따라 3~5배)에 얻는다 — 그 대가로 중간값 전부를 붙들고 있어야 한다. 학습이 추론보다 훨씬 많은 메모리를 먹는 이유가 정확히 이것이고, 그래서 이 비용을 재계산으로 되사는 그래디언트 체크포인팅이 그래프와 한 몸으로 따라다닌다.

프레임워크 관점에서는 노드마다 두 개의 규칙만 등록하면 된다. 전진용 야코비-벡터 곱(JVP)과 역방향용 벡터-야코비 곱(VJP)이다. 새 연산을 추가한다 = 노드 타입 하나와 그 두 규칙을 등록한다로 정리되며, 야코비 행렬 자체를 만들 일은 없다.2

4. 정적 그래프 대 동적 그래프[편집]

그래프를 언제 만드느냐로 생태계가 두 진영으로 갈렸다. 이 논쟁이 2016~2020년 프레임워크 전쟁의 실질적 내용이었다.

정적 그래프(define-and-run) 는 그래프를 먼저 다 짓고 나중에 데이터를 흘려보낸다. 테아노와 텐서플로 1.x의 placeholder + Session.run 이 전형이다. 장점은 전부 “미리 볼 수 있다”에서 나온다.

  • 그래프 전체를 보고 최적화할 수 있다(다음 절).
  • 모든 텐서의 형상과 수명을 미리 알 수 있으니 메모리 계획을 정적으로 세우고 버퍼를 재사용할 수 있다.
  • 파이썬 없이 배포할 수 있다. ONNX 같은 교환 포맷이 성립하는 것도 그래프가 데이터이기 때문이다 — 학습한 그래프를 파일로 떠서 전혀 다른 런타임에 얹는다.

단점은 딱 하나인데 치명적이다. 디버깅이 지옥이다. 에러가 그래프를 정의한 줄이 아니라 실행하는 줄에서 나고, 중간값을 찍어 보려면 전용 연산을 심어야 하며, 제어 흐름을 파이썬 if/for 로 못 쓰고 tf.cond·tf.while_loop 같은 전용 노드로 써야 한다.

동적 그래프(define-by-run) 는 반대로, 연산이 실제로 실행될 때마다 테이프에 한 줄씩 기록해 그래프를 사후에 완성한다. 체이너가 이 이름을 붙였고 파이토치의 autograd 가 이 방식으로 판을 뒤집었다. 파이썬 iffor 를 그냥 쓰면 그 실행 경로가 곧 그래프가 되고, 스택 트레이스가 정직하게 나오며, pdb 가 먹는다. 연구자들이 대거 넘어간 이유가 성능이 아니라 디버깅 가능성이었다는 점은 기억할 만하다. 대가는 매 반복마다 그래프를 새로 짓는 오버헤드와, 전역 최적화를 할 시야가 없다는 것.

2020년대의 결론은 “둘 다 한다”이다. 평소에는 동적으로 돌리다가 필요한 구간만 추적(tracing)해서 정적 그래프로 굳힌다. 텐서플로 2의 tf.function, JAX의 jit(추적 결과가 jaxpr 이라는 중간 표현으로 나온다), 파이토치의 TorchScript를 거쳐 2.0의 TorchDynamo 로 이어지는 계보가 그것이다. 다이나모는 파이썬 바이트코드 수준에서 그래프를 뜯어내되, 추적할 때 가정한 조건(형상·타입·분기)을 가드로 붙여 두고 가드가 깨지면 다시 추적한다. 그래도 못 잡는 부분은 그래프 브레이크로 잘라 파이썬에 되돌린다.

여기서 반드시 알아야 할 함정 하나. 추적은 실행된 경로만 기록한다. 데이터에 따라 분기하는 파이썬 if x > 0: 을 추적하면 그 입력에서 간 쪽만 그래프에 남고, 다음 입력에서 조용히 틀린다. JAX가 lax.cond·lax.scan 을 따로 요구하고, 추적 중에는 값을 들여다보는 연산이 에러를 내는 것이 바로 이 사고를 앞당겨 터뜨리기 위해서다.3

5. 그래프 최적화 — 그래프를 그래프로 바꾸기[편집]

일단 그래프가 데이터가 되면, 컴파일러가 수십 년 써 온 변환들이 그대로 적용된다.

  • 상수 접기(constant folding) — 입력에 의존하지 않는 부분 그래프를 미리 계산해 상수 노드로 치환한다. 정규화 상수, 마스크, 위치 인코딩 같은 것들이 여기서 사라진다.
  • 공통부분식 제거(CSE) — 같은 연산이 같은 입력에 대해 두 번 나오면 노드를 하나로 합친다. 사용자가 짠 코드보다 미분이 만들어 낸 그래프에 훨씬 많다. 역방향 패스는 같은 중간값을 여러 경로에서 재사용하기 때문이다.
  • 죽은 코드 제거(DCE) — 출력에서 역방향으로 도달 불가능한 노드를 지운다. 손실에 기여하지 않는 브랜치, 기울기를 요구하지 않는 텐서의 역방향 노드가 이렇게 정리된다.
  • 대수적 단순화x1x \cdot 1, x+0x + 0, exp(logx)\exp(\log x), 연속된 전치·reshape 접기. 미분이 자동 생성한 그래프에는 곱하기 1이 널려 있으므로 실질 이득이 크다.
  • 연산자 융합(fusion) — 이게 제일 크다. 원소별 연산 여러 개를 커널 하나로 합치면 중간 텐서를 메모리에 쓰고 다시 읽는 왕복이 통째로 사라진다. GPU에서 원소별 연산은 거의 전부 메모리 대역폭 한계라, 산술을 줄이는 게 아니라 왕복을 줄이는 것이 성능의 거의 전부다. XLA가 파는 물건의 본체가 이 융합이고, TVM·TorchInductor·Triton 계열도 같은 과녁을 노린다.
  • 레이아웃과 메모리 계획 — 텐서 배치(NCHW/NHWC)를 그래프 전체에 걸쳐 일관되게 고르고, 노드의 수명 구간을 보고 버퍼를 재사용한다. 정적 그래프의 마지막 남은 확고한 우위.

융합에는 미분과의 미묘한 긴장도 있다. 융합된 커널은 중간 텐서를 안 남기므로, 역방향에서 그 중간값이 필요하면 재계산하거나 융합을 포기해야 한다. 플래시어텐션이 하는 일이 정확히 “역방향에 필요한 것만 골라 남기고 나머지는 재계산하는 융합”이라는 점에서, 융합과 그래디언트 체크포인팅은 사실 같은 장부의 양변이다.

6. 시뮬레이션에서의 계산 그래프[편집]

물리 솔버를 그래프에 얹으면 생기는 문제는 딥러닝과 결이 다르다.

  • 반복 솔버를 통째로 펼치면 안 된다. 뉴턴 반복이나 공액구배를 kk 번 돌린 것을 그대로 그래프에 남기면 노드가 kk 배로 늘고, 수렴 전 중간 반복의 미분까지 섞여 들어온다. 정석은 수렴한 해에서 음함수 정리로 미분해 그래프에 노드 하나(선형계 풀이)만 남기는 것이다. 이것이 수반법·민감도 해석의 언어로 말하는 이산 수반과 같은 물건이다.
  • 시간 적분은 길다. 스텝 수천 개짜리 시뮬레이션은 그래프 깊이가 그만큼 되고, 역방향 모드의 메모리가 스텝 수에 비례해 늘어난다. 여기가 체크포인팅이 원래 태어난 자리다.
  • 희소성이 그래프 모양에 드러난다. 격자 위 스텐실 연산은 그래프에서 국소 연결로 나타나며, 야코비를 실제로 조립해야 할 때는 이 구조를 색칠로 압축해 희소행렬로 만든다.
  • 프레임워크의 배열 연산으로 표현되지 않는 알고리즘(불규칙 이웃 탐색, 비정형 메시 조립)은 노드 수가 폭발하거나 아예 그래프에 안 들어간다. 미분 가능 시뮬레이션 문서가 다루는 실패 모드의 상당수가 여기서 나온다.

7. 현업 감각[편집]

  • 그래프가 안 보이면 성능 이야기를 할 수 없다. 최적화 전후 그래프를 덤프해 노드 수와 융합 그룹을 세어 보는 것이 첫걸음이다. 파이토치 2 계열은 그래프 브레이크 개수를, JAX는 재추적 횟수를 먼저 본다. “컴파일했는데 안 빨라졌다”의 90%는 그래프가 잘게 쪼개진 것이다.
  • 형상이 매 스텝 바뀌면 정적화의 이득이 증발한다. 가변 길이 입력은 패딩·버킷팅으로 형상 종류를 유한하게 묶는 게 국룰. 안 그러면 추적을 매번 다시 한다.
  • 테이프가 안 죽는 것이 가장 흔한 메모리 누수다. 기울기가 필요 없는 텐서를 로그나 리스트에 그냥 쌓아 두면 그래프 전체가 참조에 매달려 안 풀린다. 평가 루프에 no_grad 를 안 걸어 OOM 나는 사고는 매주 어딘가에서 재현된다.
  • 미분 불가능점은 그래프가 안 알려 준다. max, abs, 클램프는 노드로서는 멀쩡하고 한쪽 도함수를 성실하게 내놓는다. 그 값이 물리적으로 의미가 있는지는 사람이 판단해야 한다.
  • 그래프는 검증 대상이기도 하다. 융합·재배치·재계산은 부동소수점 결합법칙을 건드리므로 비트 단위 재현성이 깨질 수 있다. 결과가 미묘하게 달라지면 컴파일 플래그부터 의심하는 게 부동소수점 연산 쪽의 오랜 교훈과 정확히 같다.

8. 관련 문서[편집]

9. Footnotes[편집]

  1. Wengert, R. E. (1964). “A simple automatic derivative evaluation program”. CACM 7(8). 두 쪽짜리다. 60년 뒤 수십억 달러짜리 산업의 자료구조가 될 거라고는 저자도 몰랐을 것이다. 역방향으로 훑는 아이디어는 린나인마(1970)의 석사 논문에, 기울기 전체를 한 번에 뽑는 형태는 스펠펜닝(1980)의 박사 논문에 나온다. 딥러닝이 “발명”한 것은 이 중 하나도 없다.

  2. 그래서 프레임워크에 새 연산을 기여할 때 “야코비를 구해 오세요”라고 하지 않고 “VJP를 구현하세요”라고 한다. n×mn \times m 행렬을 만들지 말고 그 행렬과 벡터의 곱만 계산하라는 뜻인데, 이 구별을 놓치고 성실하게 야코비를 조립했다가 메모리를 태워 먹는 신입이 주기적으로 등장한다.

  3. JAX에서 추적 중인 값을 if 조건이나 print 에 넣으면 나오는 그 유명한 TracerBoolConversionError 가 이것이다. 처음 만나면 프레임워크가 심술을 부린다고 느끼지만, 사실은 “네가 방금 그래프에 굽고 있는 그 분기는 이 입력에서만 맞다”고 미리 비명을 질러 주는 것이다. 조용히 틀린 그래프를 받는 것보다 백배 낫다.