[Pytorch] 분산 학습 1: GIL의 기본 개념

Date:     Updated:

카테고리:

Global Interpreter Lock (GIL) 이란?

GIL(Global Interpreter Lock)은 파이썬에만 존재하는 고유한 메커니즘으로, 여러 스레드가 동시에 파이썬 바이트코드를 실행하지 못하도록 제어하는 일종의 잠금 장치(Mutex)이다. 즉, 파이썬 프로그램이 여러 개의 스레드를 생성하더라도 한 시점에는 오직 하나의 스레드만이 인터프리터의 제어권과 자원을 독점적으로 사용할 수 있도록 제한하며, 나머지 스레드는 대기 상태로 전환된다. 이러한 구조는 메모리 관리의 일관성과 안전성을 확보하기 위한 설계이지만, 멀티코어 환경에서 병렬 실행 성능을 저하시킨다는 한계도 가지고 있다.

  • 정의: CPython 인터프리터 수준에서 파이썬 객체에 대한 메모리 안전성을 보장하기 위해 동시에 오직 한 스레드만 파이썬 바이트코드를 실행하게 하는 전역 락이다.
  • 목적: 간단한 참조 카운팅 기반 메모리 관리와 C API의 구현 난이도를 낮추어 CPython을 안정적으로 유지하려는 설계 선택이다.
  • 효과: 스레드가 여러 개여도 동시에 실행되는 파이썬 바이트코드는 하나이므로, 순수 파이썬 CPU-바운드 병렬화는 성능 이득이 거의 없다



Python에서 멀티스레딩

1

일반적으로 멀티스레딩이라고 하면 왼쪽과 같이 두 개의 스레드가 동일한 작업에 대해 병렬적으로 동시에 실행을 하는 것을 말한다. 하지만 파이썬에서는 오른쪽과 같이 동작한다.

GIL이 스레드끼리 공유하는 프로세스의 자원을 이름 그대로 Global하게 Lock 해버리고 단 하나의 스레드에만 이 자원이 접근하는 것을 허용한다. 따라서 그림과 같이 멀티스레드라 하더라도 한 번에 하나의 스레드만 실행하게 된다. 이는 결론적으로 스레드 간에 컨텍스트 스위칭 비용을 발생시키고, 멀티스레드가 싱글스레드와 비슷한 성능을 보이거나 오히려 떨어지게 되는 결과를 만든다. 정리하자면, 다음과 같은 이유로 파이썬에서 멀티스레딩에 문제가 발생된다.

  • 병렬 실행 불가: CPU-바운드 순수 파이썬 코드는 스레드가 여러 개라도 실제로는 한 번에 하나만 실행되어 코어 확장 이득이 없다.
  • 컨텍스트 스위칭/락 오버헤드: GIL 획득/반납과 스레드 스케줄링이 잦아져 오히려 더 느려질 수 있다.
  • 락 경합과 공정성 이슈: 스레드 수가 많고 작업이 미세하면 락 경합으로 지연이 커진다.
  • 디버깅 난이도 증가: 레이스 컨디션, 데드락, 교착 상태가 파이썬 레벨과 C 레벨에서 얽혀 문제 재현과 분석이 어렵다.

GIL 때문에 파이썬에서 멀티스레딩을 할 필요가 없는 것 처럼 보일 수 있지만, CPU 연산이 큰 비중을 차지하는 경우가 아닌, I/O 작업이 큰 비중을 차지하거나 sleep으로 일정 시간 대기해야 하는 경우 멀티스레딩이 더 좋은 성능을 보이게 된다. 이는 입력 대기시간이나 sleep으로 대기하는 동한 컨텍스트 스위칭이 이루어지기 때문이다. 구체적으로 다음과 같은 경우 파이썬에서 멀티스레딩이 유요하다.

  • I/O-바운드: 네트워크 크롤링, DB/파일 I/O 대기 등은 대기 시간 동안 GIL 해제로 다른 스레드가 실행되어 처리량이 증가한다.
  • 확장 모듈 기반 연산: NumPy/BLAS, PyTorch, Numba/Cython 등 내부에서 GIL을 해제하고 C/CPU/GPU 병렬화를 하는 경우, 스레딩이 실질 병렬 성능에 기여한다.

GIL의 동작 방식

  • 단일 실행권: 스레드는 바이트코드를 실행하기 전에 GIL을 획득해야 하며, 실행 중에는 다른 스레드가 진입할 수 없다.
  • 컨텍스트 스위칭: 인터프리터는 일정 이벤트(바이트코드 수, 타이머 등)마다 GIL을 양보하여 다른 스레드가 실행 기회를 얻도록 한다.
  • I/O에서의 해제: 파일/소켓/네트워크 등 블로킹 I/O 호출 시 GIL을 해제하고, I/O가 끝나면 다시 획득한다.
  • C/확장 모듈: NumPy, PyTorch, OpenCV 같은 확장 모듈은 자체의 C/CUDA 커널에서 GIL을 해제하고 내부 병렬화를 수행하기 때문에, 파이썬 스레드 위에서도 실제 병렬 실행이 가능하다.



GIL이 필요한 이유

파이썬에서 GIL이 필요한 이유는 파이썬에서 메모리를 관리하는 방법을 이해해야한다. 우리가 일반적으로 파이썬 코드를 작성할 경우, 모든 것은 파이썬에서 객체(objective)가 된다. 그리고 파이썬은 이러한 객체들에 대해 참조 횟수(Reference Count)를 저장하고 있다. 이 값은 각 객체들이 참조되는 횟수를 나타내며, 참조 여부에 따라 알아서 증감된다.

어떤 객체에 대한 모든 참조가 해제되어 참조 횟수가 0이 된다면, 파이썬에서 GC(Garbage Collector)가 그 객체를 메모리에서 삭제시킨다. 따라서 Reference Count의 값은 항상 정확해야 적절하게 GC가 처리할 수 있다.

하지만 만약에 여러 스레드가 동시에 한 객체에 접근하게 되면 해당 객체의 참조 횟수에 대해 레이스 컨디션 (Race Condition, 하나의 자원을 동시에 사용하게 될 때 기대하지 않은 결과가 발생하는 상황)이 발생하게 되고, 이는 GC의 동작에 충돌을 야기할 수 있다.



Reference

Blog: 파이썬(python) - GIL(Global Interpreter Lock)

Pytorch 카테고리 내 다른 글 보러가기

댓글 남기기