본문 바로가기
C.W.K.
Stream
Lesson 05 of 06 · published

스왑 절벽

~14 min · memory, swap, ssd, memory-pressure, compressor, measured

Level 0스펙 시트 훑는 사람
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"메모리는 서서히 바닥나지 않아. 한꺼번에 바닥나고, 그다음엔 SSD가 네 메모리 버스야."

가장자리 너머엔 뭐가 있나

맥은 풀이 찼다고 메모리 할당을 거부하는 법이 없어. 계층이 둘 더 있거든. 첫째는 압축기. 최근에 안 건드린 페이지를 제자리에서 압축해서 CPU 시간을 공간과 바꾸고, 산수로는 아닐 텐데도 맥이 한참 지나서까지 멀쩡하게 느껴지는 이유가 이거야. 둘째는 스왑. 페이지를 SSD에 써두고 필요할 때 다시 읽어. 둘 다 들여다보기 전엔 안 보이고, 둘 다 한 특정 워크로드엔 재앙이야. 토큰마다 가중치를 다시 읽는 모델. 어느 계층도 메모리 속도 근처로는 흘려보낼 수 없거든.

절벽의 높이는 메모리 대역폭과 SSD 대역폭의 비율이야. GPU 트랙은 Air의 달성 메모리 대역폭을 93–97 GB/s로 쟀어. 메모리보다 큰 파일이라 페이지 캐시가 도울 수 없는, 20 GiB 캐시 안 된 순차 SSD 읽기는 2.85 GB/s로 돌았어. 34배야. Mac Studio에선 메모리 쪽이 6배에서 8배 빠르고 SSD는 안 그러니까, 거기 절벽은 100배 이상이야. 가중치의 10분의 1이 그 가장자리 너머에 있는 모델은 4분의 1 속도로 디코드하고(코드 블록의 공식으로 Air 숫자를 넣으면 6.0에서 1.5 tok/s), 절반이 넘어간 모델은 초당 3분의 1토큰이야. 실질적으로 돌고 있는 게 아니지.

가장자리에서 한 걸음, Air

실험 트랙은 24 GB Air를 정확히 가장자리까지 밀었어. 27B 모델, 디스크에 16.05 GB, 피크 15.78 GB는 Air의 17.8 GiB GPU 작업 집합 안에 앉고, 돌았어. 초당 6.09토큰, 작은 모델들이 달성한 것과 같은 88 GB/s 실효 대역폭으로. 절벽 없음. 하지만 그 뒤에 읽은 vm.swapusage는 스왑 1.7 GB 사용 중, 압축기가 추가로 0.9 GB를 붙들고 있음을 보여줬어. GPU 작업 집합 자리를 내려고 macOS가 기계의 다른 모든 걸 디스크로 밀어낸 거야. 사다리의 다음 모델인 20 GB 전문가 혼합 모델은 17.8을 권장하는 기계에서 19.9 GB 작업 집합이 필요하고, 이 퀘스트는 그걸 돌리지 않았어. 일부러. 절벽에 선 기계는 응답을 멈출 수 있고, Air는 시험대가 아니라 누군가의 악기니까. 그게 가장자리의 정직한 모양이야. 들어가는 마지막 모델은 뒤에 아무것도 안 남기고, 안 들어가는 첫 모델은 느려지는 게 아니라 사라져.

일어나기 전에 보기

맥이 어디 서 있는지는 세 가지 읽기가 말해줘. vm_stat은 페이지가 얼마나 비어 있고, 활성이고, wired고, 압축됐고, 스왑됐는지 보고해. sysctl vm.swapusage는 스왑 사용량을 보고해. 활성 상태 보기의 메모리 압력 그래프는 압축기가 열심히 돌면 노랑, 스왑이 돌면 빨강으로 변해. 추론 호스트의 규칙은 모델이 올라가서 최대 컨텍스트로 생성하는 동안 메모리 압력이 초록으로 유지되는 거야. 노랑은 다음 할당이 동전 던지기라는 뜻이고, 빨강은 모델이 이미 일부 디스크에 있고 토큰마다 그 값을 치르고 있다는 뜻이야. 코드 블록이 카운터를 읽고 네가 이름 대는 모델에 절벽 산수를 해.

Code

cliff.py — 캐시 안 된 SSD 대역폭, 기계의 스왑 상태, 넘침의 비용·python
#!/usr/bin/env python3
"""Measure SSD sequential read with the page cache bypassed (F_NOCACHE, so the file
need not exceed RAM; 20 GiB is plenty), read the swap counters, and compute what spilling costs.
Usage: cliff.py <file GiB> <model GB> <spill GB>"""
import fcntl, os, subprocess, sys, time

file_gib = float(sys.argv[1]) if len(sys.argv) > 1 else 20
model_gb = float(sys.argv[2]) if len(sys.argv) > 2 else 16.05
spill_gb = float(sys.argv[3]) if len(sys.argv) > 3 else 1.6

path = os.path.expanduser("~/cliff-test.bin"); size = int(file_gib * 2**30)
if not os.path.exists(path) or os.path.getsize(path) != size:
    with open(path, "wb") as f:
        buf = os.urandom(1 << 20)
        for _ in range(size >> 20):
            f.write(buf)
fd = os.open(path, os.O_RDONLY); fcntl.fcntl(fd, fcntl.F_NOCACHE, 1)
t = time.perf_counter(); n = 0
while (b := os.read(fd, 8 << 20)):
    n += len(b)
ssd = n / (time.perf_counter() - t) / 1e9; os.close(fd); os.remove(path)
print(f"SSD sequential read, {file_gib:.0f} GiB uncached: {ssd:.2f} GB/s")

print(subprocess.run(["sysctl", "vm.swapusage"], capture_output=True, text=True).stdout.strip())

mem = 97.0   # your achieved memory bandwidth from stream.py (air: 97 GB/s)
fit = mem / model_gb
spilled = 1 / ((model_gb - spill_gb) / mem + spill_gb / ssd)
print(f"{model_gb:.1f} GB model: fits -> ceiling {fit:.1f} tok/s; {spill_gb:.1f} GB spilled to SSD -> {spilled:.2f} tok/s  ({fit/spilled:.0f}x slower)")

# air, 2026-09-15: SSD 2.85 GB/s; after the 27B run vm.swapusage used = 1723 MB;
# 16.05 GB model with 1.6 GB spilled: 6.0 -> 1.5 tok/s (4x); with 8 GB spilled: 0.34 tok/s
셸에서 보는 카운터들·bash
vm_stat | grep -E "Pages (free|active|wired|occupied by compressor|swapped)"
sysctl vm.swapusage
# air, after the 27B run, 2026-09-15:
# Pages occupied by compressor:  54804     <- ×16 KB = 0.9 GB compressed
# vm.swapusage: total = 3072.00M  used = 1723.56M  free = 1348.44M

# Activity Monitor > Memory > Memory Pressure: green = fine, yellow = compressor busy, red = swapping.

External links

Exercise

네 맥에서 cliff.py를 돌려(기본 20 GiB 파일이면 충분해. F_NOCACHE가 페이지 캐시를 우회하니까 메모리보다 클 필요는 없어). mem엔 네 달성 대역폭을 넣고. SSD 수치와 메모리 대 SSD 비율을 카드에 추가해. 그다음 네가 돌리는 제일 큰 모델의 10%가 넘쳤을 때 디코드 속도를 계산하고 결정해. 빨간 메모리 압력 그래프에 대한 맞는 대응은 더 작은 모델이야, 더 짧은 컨텍스트야, 더 큰 맥이야?
Hint
거의 언제나 더 작은 모델이나 더 짧은 컨텍스트야. 비율이 너무 커서 어떤 관용도 못 버티거든. 더 큰 맥이 답인 건 네가 필요한 컨텍스트에서의 그 모델이 기계를 소유하는 이유일 때뿐이야. 그게 구성기에서 정하는 용량-한-번 레슨이고.

Progress

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

댓글 0

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

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