- _ops_conv2d.pyd: pybind11로 묶인 저수준(native) 바인딩. 포인터·스트림·WS(워크스페이스)를 그대로 받아 커널을 쏘는 층.
- ops/conv2d.py: 그 위를 감싸는 파이썬 래퍼(헬퍼). 입력 검증, shape 계산, attrs 생성, Z_saved/WS 기본값 준비, out= 할당 등을 처리하는 인간 친화적 API.
1) 호출 인터페이스
- pyd(바인딩):
- 인자: x_ptr, x_shape, w_ptr, w_shape, y_ptr, y_shape, bias_ptr|None, z_ptr|None, attrs, stream, dCol_ptr, ...
- 완전 로우레벨. 포인터 정수, 스트림 포인터, WS 포인터를 그대로 줘야 함.
- 내부 동적 할당 없음(그래야 그래프 캡처 안전).
- ops 헬퍼(conv2d.py):
- 인자: X, W, B=None, stride=..., padding=..., act=..., save_z=..., Z_saved=None, out=None, stream=None
- cupy ndarray만 주면 됨. shape 추론, attrs 구성, Z_saved/WS를 자동 준비해줌.
- 개발자 경험은 좋지만, 기본 구현대로면 호출 때마다 WS를 새로 만든다는 점 유의.
2) 그래프 캡처/성능 관점
- pyd 직호출:
- 장점: 할당 제로, 모든 버퍼(출력/WS)를 밖에서 재사용하도록 강제 → CUDA Graph 캡처 최적.
- 단점: 사용 난이도↑(포인터·스트림·WS 규약을 전부 맞춰야 함).
- ops 헬퍼:
- 장점: 코드가 깔끔, 검증과 에러 메시지가 친절, 빠른 프로토타입에 최적.
- 단점: 지금 구현은 각 호출마다 cp.empty로 WS를 새로 할당 → 캡처 안에서 쓰기엔 위험 / 반복 호출 성능에 부담.
- 해결책: 그래프 실행기처럼 노드에 WS를 미리 넣어두고, 헬퍼를 건너뛰거나(네가 한 fast-path) 헬퍼가 외부 WS 포인터를 받도록 확장.
3) 안정성과 호환성
- ops 헬퍼가 있으면:
- 내부적으로 pyd의 시그니처가 약간 바뀌어도 헬퍼가 어댑터가 되어 상위 API를 안정화할 수 있음(ABI 문제 완충).
- pyd가 로딩 안 되면(미빌드/의존 DLL 없음 등) 명시적 ImportError와 안내를 줄 수 있음(지금 메시지 좋음).
- 동일 함수에 fallback 구현(예: 순수 cupy)도 넣을 수 있음.
- pyd 직호출만 쓰면:
- 빠르지만, ABI/시그니처 바뀌면 상위 코드가 곧바로 깨짐.
- 디버깅시 네이티브 경로로 바로 내려가야 해서 난이도↑.
1️⃣ 바인딩 레벨에서 “파라미터 전달 방식”을 바꾸면 생기는 이득들
✅ (A) 파라미터를 Python → C++ 변환 전에 구조체로 묶기
지금 Conv2DAttrs 처럼 stride/padding/dilation 등을 미리 struct로 묶어서 넘기고 있죠?
이게 이미 성능 최적화의 1단계예요.
- ✔ 장점
- pybind11이 각 인자를 개별 변환하지 않고 한 번만 변환 (오버헤드 ↓)
- GPU 런처로 그대로 전달 가능 → __constant__ 메모리에 바로 복사할 수도 있음
- Graph 캡처 시 attrs 객체는 host 상수로 고정 → 재사용 완벽하게 가능
- 🚀 팁
- Conv2DAttrs를 __constant__ 메모리에 로드하는 커널 버전을 만들면
파라미터 fetch latency가 10~30% 줄어요 (특히 소규모 kernel).
- Conv2DAttrs를 __constant__ 메모리에 로드하는 커널 버전을 만들면
✅ (B) Conv2DWorkspaceFwd / Conv2DWorkspaceBwd 같은 워크스페이스 포인터 구조체를 커널 인자로 직접 전달
Conv2DWorkspaceFwd ws{};
ws.dCol = reinterpret_cast<float*>(dCol_ptr);
...
auto st = Conv2DCudaLaunch(X, W, Bptr, Y, attrs, stream, Zptr, &ws);
이렇게 한 번 거치는데,
→ Conv2DCudaLaunch가 다시 내부 커널로 넘길 때 구조체 통째로 __device__ 메모리로 전달한다면
→ 각 포인터를 레지스터에 펼칠 필요 없이 한 번에 로드 가능합니다.
즉,
- CPU→GPU로 포인터를 10개 넘기던 걸
- 구조체 한 덩어리(sizeof(ws) ≈ 64B)로 전달하면
커널 파라미터 스택 압박 ↓, 커널 런칭 latency ↓
그리고 ptxas에서 register spill도 덜 발생
✅ (C) Python에서 넘기는 “반복되는 상수”를 바인딩에 캐싱
예: stream_ptr, attrs, device_index, groups, with_bias 같은 게 매번 동일하다면
→ pybind11 바인딩에서 전역 static 캐시를 두고,
→ Python 쪽은 “한 번 등록한 handle id”만 넘겨도 됨.
이건 그래프 실행기에서 수천 노드 캡처할 때 오버헤드 차이 큼.
'dev_AI_framework' 카테고리의 다른 글
| 현재 CUDA Graph Capture 과정 (0) | 2025.10.07 |
|---|---|
| backward 까지 capture 하려면 (0) | 2025.10.07 |
| graph_executor_v2 구성 (0) | 2025.10.07 |
| Graph Executor v2 — Forward 및 Training Graph 설계 문서 (0) | 2025.10.07 |
| GEMM(+bias+act)에서 Z(pre-activation) 저장/활용 설계 (0) | 2025.10.06 |