C.W.K.
Stream
Lesson 04 of 07 · published

Lazy evaluation — 한 번만 머리 바꾸면 되는 그것

~18 min · lazy-eval, graph, mx.eval

Level 0Curious
0 XP0/51 lessons0/15 achievements
0/100 XP to next level100 XP to go0% complete

한 문단의 shift

NumPy 와 PyTorch 의 eager 모드에선, array 코드의 모든 줄이 그때 작업을 해. y = x * 2 + 1 가 결과 계산하고 저장. y 절대 안 쓰면 작업 낭비. MLX 에선 그 같은 줄이 계산을 묘사하는 작은 graph 를 빌드. 작업은 나중에 일어나 — 네가 (명시적 또는 implicit) 값을 요청할 때.

그게 shift 전부야. MLX 의 lazy evaluation 의 다른 모든 건 이 한 결정의 결과.

실제 계산을 트리거하는 것

Explicit: 하나 이상의 array 에 mx.eval(...) — materialize 의 canonical 방법.

Implicit: 구체적 값 읽어야 하는 무엇이든. print(array) 가 eval 트리거 (그래서 graph 아니라 숫자 봐). Scalar 에 .item() 이 eval 트리거. NumPy array 또는 Python list 로 변환이 eval 트리거. Array 에 indexing 도 eval 트리거 — indexing 은 값을 알아야 하니까.

그래서 REPL 에서 작은 예제 만질 때 MLX 가 NumPy 처럼 느껴지는 거야 — 모든 print 가 조용히 작업을 강제하니까, laziness 가 안 보여. 많은 op 을 함께 묶고 더 적은 GPU dispatch 로 fuse 되길 원할 때만 중요해져.

왜 MLX 가 이걸 굳이 하나

  • Kernel fusion — MLX 가 op graph 보면 가끔 그것들을 단일 Metal kernel 로 fuse 가능. mx.compile (lesson 7) 이 이것에 기댐. Eager 모드는 못 해.
  • Function transformmx.grad, mx.vmap 깨끗하게 동작 — side-effect 호출 시퀀스가 아니라 전체 함수를 graph 로 받으니까. JAX 가 같은 trade 해.
  • 메모리 효율 — 절대 안 읽히는 intermediate 는 통째로 skip 가능.

디버깅 함의

Print 할 때 eval 이 implicit 이니까, NumPy 의 "step-through" 직관이 REPL 에서 여전히 통해 — 물어보면 숫자 보일 거. 함정은 print 없는 production 코드가 네가 예상하는 것보다 훨씬 긴 시퀀스에 걸쳐 lazy 유지할 수 있다는 것 — 에러가 체인 끝의 mx.eval 까지 안 드러나. 두 디버깅 관용구에 익숙해져 — triage 중 의심 intermediate 에 mx.eval 뿌려, 그리고 REPL 에선 같은 효과로 print 뿌려.

Code

Eager 처럼 보이고, lazy 처럼 동작·python
import mlx.core as mx

x = mx.array([1.0, 2.0, 3.0])
y = x * 2 + 1                  # builds a graph; no kernel has run
# At this point, y is a graph node, not concrete numbers.

# Implicit eval — print materializes
print('y :', y)                 # → array([3, 5, 7], dtype=float32)
Explicit eval — print 없는 큰 시퀀스용·python
import mlx.core as mx

a = mx.random.normal((1024, 1024))
b = mx.random.normal((1024, 1024))

# All three lines below build a graph; nothing executes yet.
c = a @ b
d = c + a
e = mx.tanh(d)

# Force the whole graph to execute, in one go (better fusion opportunity).
mx.eval(e)

print('e mean:', float(e.mean()))   # implicit eval, but e is already materialized
한 mx.eval 호출에 여러 출력·python
import mlx.core as mx

x = mx.array([1.0, 2.0, 3.0])
sq = x ** 2
sm = sq.sum()
mn = sq.mean()

# Materialize all three at once — MLX can schedule them efficiently together.
mx.eval(sq, sm, mn)
print('sq:', sq, 'sum:', float(sm), 'mean:', float(mn))

External links

Exercise

(512, 512) random array 위에 다섯 op 체인 만들기 (예 matmul, add, tanh, sum, mean) 그리고 두 버전 timing — (a) 끝에 한 번 큰 mx.eval, (b) 모든 줄 후 mx.eval. time.perf_counter() 써, 각 세 번 돌려, 중앙값. 네 머신에서 의미 있게 달라? 그래야 해 — 첫 번째 버전이 두 번째가 못 받는 fusion 이익 받으니까.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.