TP rank, vLLM 워커, 다중 ring
변경 검증 가이드 (다음 fetch 후):
git log eaa2bfee..HEAD -- lmcache/v1/distributed/l2_adapters/raw_block_l2_adapter.py위에 커밋이 잡히면
per_tp_device_pathsMP 거부 로직 (라인 147-150) 이 살아있는지, MP 에서도 지원하도록 바뀌었는지 재확인 필요. 후자면 본 문서의 "MP 에서 깨지는 이유" 섹션 통째로 다시 써야 함.
"워커" 라는 단어가 두 가지 다른 레벨에서 쓰여서 혼동되기 쉬움. 분리해서 정리.
TP (Tensor Parallelism)
큰 모델을 여러 GPU 에 쪼개서 돌리는 병렬화 기법.
예: Llama-70B 를 GPU 4 개로
원본 가중치: W: [10000 × 8192]
TP=4 로 쪼개기:
GPU 0: W[:, 0:2048] ← 1/4
GPU 1: W[:, 2048:4096]
GPU 2: W[:, 4096:6144]
GPU 3: W[:, 6144:8192]
각 GPU 가 자기 몫만 가지고 매 레이어마다 NCCL 로 결과를 합쳐 다음 레이어로 넘김.
rank
"몇 번째 GPU/워커냐" 를 가리키는 정수 ID.
- TP=4 → rank 0, 1, 2, 3
- "TP rank 2" = "4 개 중 3 번째 GPU"
분산 컴퓨팅 일반 용어 (MPI 에서도 동일).
"워커" 의 두 가지 의미
1. vLLM 워커 (프로세스 레벨)
"GPU 1개 = OS 프로세스 1개".
[Llama-70B + TP=4 인 경우, 한 노드]
Python 프로세스 0 ←→ GPU 0 (rank 0)
Python 프로세스 1 ←→ GPU 1 (rank 1)
Python 프로세스 2 ←→ GPU 2 (rank 2)
Python 프로세스 3 ←→ GPU 3 (rank 3)
- 하나의 모델을 4개 프로세스가 나눠서 서빙
- 각 프로세스는 모델 weight 의 1/4 만 들고 있음
- 매 레이어마다 NCCL 로 GPU 끼리 결과 합침
- 이 4개의 Python 프로세스 각각을 vLLM 에서 "worker" 라고 부름
한 줄 요약: "vLLM worker = 1 GPU 를 담당하는 Python 프로세스 1개"
2. io_uring 워커 (스레드 레벨, RawBlockDevice 안)
vLLM 과 무관한, Rust 쪽 스토리지 IO 엔진 내부의 worker thread.
[하나의 RawBlockDevice 인스턴스 안]
애플리케이션 스레드 → SQE 제출
↓
io_uring 인스턴스 1개
↓
worker 스레드 1개 ── /dev/nvme0n1 로 실제 read/write
- 이 "worker" 는 OS 프로세스가 아니라 RawBlockDevice 객체 안의 스레드 1개
- 단일 ring → 단일 worker thread → 단일 코어 병목 (다중 ring 으로 확장 후보)
두 워커가 어떻게 연결되나 (non-MP 케이스)
per_tp_device_paths 가 왜 non-MP 에서만 의미 있는지:
vLLM 워커 프로세스 0 (rank 0)
└─ LMCacheEngine (이 프로세스 안에 in-process)
└─ RawBlockDevice
└─ io_uring worker 스레드 1개
└─ /dev/nvme0n1 ← per_tp_device_paths[0]
vLLM 워커 프로세스 1 (rank 1)
└─ LMCacheEngine
└─ RawBlockDevice
└─ io_uring worker 스레드 1개
└─ /dev/nvme1n1 ← per_tp_device_paths[1]
... (rank 2, 3 도 동일)
핵심:
- 각 vLLM 워커 프로세스가 자기 LMCacheEngine 을 in-process 로 들고 있음
- 그래서 "내가 rank 0 이면 nvme0 에 쓴다" 가 자연스럽게 됨
- LMCacheEngine 안의 io_uring worker thread 와 vLLM worker process 는 다른 레벨, 우연히 같은 단어를 씀
MP 에서 깨지는 이유
vLLM 인스턴스 A
워커 프로세스 0 ─┐
워커 프로세스 1 ─┤
워커 프로세스 2 ─┼── ZMQ ──→ 별도 LMCache 서버 프로세스 (1개)
워커 프로세스 3 ─┘ │
└─ RawBlockDevice
- LMCache 가 vLLM 워커 안에 있는 게 아니라 분리된 별도 프로세스로 떠있음
- 그 1개 서버가 rank 0,1,2,3 요청을 다 받음 (심지어 다른 vLLM 인스턴스도)
- "rank → device" 매핑이 무의미 →
per_tp_device_paths에러 - 자연스러운 모델은 공유 풀 + hash sharding
근거 코드: lmcache/v1/distributed/l2_adapters/raw_block_l2_adapter.py:147-150
if "per_tp_device_paths" in d:
raise ValueError(
"per_tp_device_paths is not supported in MP raw_block mode"
)
KV 캐시도 TP 따라 쪼개짐
한 토큰의 KV 전체: K[1, 8192] + V[1, 8192]
GPU 0 가지는 부분: K[:, 0:2048], V[:, 0:2048]
GPU 1 가지는 부분: K[:, 2048:4096], ...
각 워커는 자기 GPU 의 자기 1/4 KV 만 가짐. LMCache 에 저장할 때도 워커별로 자기 1/4 만 저장.
다중 ring (io_uring 확장)
io_uring 컨텍스트의 얘기. vLLM 프로세스 분배와는 무관, 한 프로세스 안에서 CPU 코어를 더 쓰기 위한 것.
단일 ring 의 진짜 병목
vLLM 워커 0 (Python)
↓ KV 1MB write 요청 1000개 쏟아짐
LMCache → RawBlockDevice
↓
io_uring 1개 (queue_depth=256)
↓
worker 스레드 1개 ◀── 이놈이 CPU 코어 1개로
SQE 제출 + CQE 폴링 + 콜백 처리 다 함
↓
/dev/nvme0n1 (e.g. 14 GB/s 가능한 PCIe5 SSD)
문제: SSD 는 14 GB/s 뽑을 수 있는데, worker 스레드 1개가 코어 1개로 다 처리하려니 100% 바빠도 7 GB/s 에서 멈춤. 디바이스가 놀고 있음.
이유:
- 매 IO 마다 syscall, 메모리 카피, completion 콜백 등 CPU 일이 적지 않음
- 코어 1개의 single-thread 처리량이 천장
- 특히 작은 IO 가 많을수록 (KV chunk 같은 1MB 단위) IOPS 가 부족해짐
다중 ring 하면 뭐가 좋냐
vLLM 워커 0 (Python)
↓ KV 1MB write 요청 1000개
LMCache → RawBlockDevice
↓ 슬롯 ID 로 hash 분배
├─ ring 0 ─ worker 스레드 0 (코어 0) ─┐
├─ ring 1 ─ worker 스레드 1 (코어 1) ─┤
├─ ring 2 ─ worker 스레드 2 (코어 2) ─┼─ /dev/nvme0n1
└─ ring 3 ─ worker 스레드 3 (코어 3) ─┘
- CPU 코어 4개 동시에 SQE 제출/CQE 처리 → SSD 가 뽑을 수 있는 throughput 끝까지
- 각 ring 은 lock-free (자기 SQ/CQ만 봄) → 코어간 캐시 라인 핑퐁 없음
- NUMA: ring 을 SSD 가 붙어있는 NUMA 노드의 코어에 핀 → 메모리 트래픽 로컬
한 줄 요약: 다중 ring = 디바이스 throughput 을 다 뽑기 위한 CPU 코어 병렬화, vLLM 분배와는 무관.
TP 분산 vs 다중 ring (레이어 다름)
| 레이어 | 단위 | 효과 |
|---|---|---|
| vLLM 워커 (TP) | 프로세스 = GPU 1개 | KV 데이터를 1/N 로 나눠 다른 SSD 로 — 데이터 분산 |
| 다중 ring | 한 프로세스 안 스레드 = 코어 1개씩 | 같은 SSD 의 throughput 을 끝까지 — CPU 분산 |
- TP 분산만 있으면: 각 SSD 의 절반밖에 못 씀 (단일 ring 병목)
- 다중 ring 만 있으면: SSD 1개 throughput 만 — 다른 SSD 못 씀
- 둘 다 하면: 4 SSD × 4 ring = 16 코어 동원 → 전체 throughput 풀로
non-MP vs MP 에서 다중 ring 효과
- non-MP: 워커 프로세스가 이미 N개라 그나마 코어 N개는 쓰는 셈. 각 워커당 SSD 풀스피드 못 뽑을 때만 추가 효과.
- MP: LMCache 서버 프로세스 1개가 모든 vLLM 워커 + 다른 인스턴스의 요청까지 다 받음 → 단일 ring 이면 그 서버 프로세스의 코어 1개가 진짜 절벽. 다중 ring 효과 큼.
[MP — 다중 ring 이 절실한 시나리오]
vLLM A 워커 0,1,2,3 ─┐
vLLM B 워커 0,1 ─┼─→ LMCache 서버 (1 프로세스)
├─ ring 0 (코어 0)
├─ ring 1 (코어 1)
├─ ring 2 (코어 2)
└─ ring 3 (코어 3)
↓
/dev/nvme0..3 풀
근거: private/docs/notes/raw_block_line.md (C6 항목)
단일 ring + 단일 worker (Rust 단계) | TB 급 throughput 한계. 멀티 ring / NUMA 친화 스케줄 필요
정리
| 용어 | 의미 | 어디 살아있나 |
|---|---|---|
| vLLM worker | GPU 1개 담당하는 Python 프로세스 | OS 프로세스 |
| TP rank | 그 워커의 번호 (0~N-1) | 환경변수 / dist init |
| LMCacheEngine | KV 캐시 엔진 | non-MP 면 vLLM 워커 안, MP 면 별도 프로세스 |
| io_uring worker | Rust RawBlockDevice 안 IO 스레드 | LMCacheEngine 안의 스레드 |
| 질문 | 답 |
|---|---|
| TP? | Tensor Parallelism — 모델을 여러 GPU 에 쪼개서 돌림 |
| rank? | "몇 번째 GPU/워커냐" 정수 ID |
| vLLM 워커 = GPU 1 개? | TP 에서는 그렇다 |
| 다중 ring = vLLM 분배? | ❌ |
| 다중 ring = ? | 한 프로세스 안에서 CPU 코어를 여러 개 써서 SSD throughput 다 뽑기 |
| 단일 ring 의 병목? | worker 스레드 1개 = CPU 코어 1개 → SSD 를 다 못 씀 |
| MP 에서 다중 ring 효과? | 큼 — LMCache 서버 1개에 부하 집중 |