ML/MLOps

기반 다지기 - Python 편

minturtle 2024. 12. 20. 22:40
반응형

개요

  • 이번에는 Python의 구조를 알아보는 시간을 가져보도록 하겠습니다.

PVM의 내부 구조

  • Python도 Java와 마찬가지로 VM 내에서 동작하는 것으로 알려져 있습니다. 그럼 Python VM(PVM)이 어떤 구조로 되어 있고 어떤식으로 Python 파일을 실행하는지 알아보도록 하겠습니다.

Python 파일이 실행될 때까지

  • Python이 Virtual Machine에서 실행되는 것은 맞지만, .py 파일이 PVM 내에서는 실행되지 못한다고 합니다. 따라서 Java의 .java 파일이 Java Interpreter이 해석할 수 있게 .class로 1차 컴파일 되듯, Python의 .py 파일도 VM이 해석할 수 있도록 .pyc 파일로 컴파일 되어야 한다고 합니다.
  • 이렇게 .pyc로 컴파일된 파일은 Python Interpreter에 의해 기계어로 변환되어 실행되게 되는 Java와 매우 흡사한 구조를 가지고 있습니다. 
    • 파이썬에서는 .py → 컴파일 → .pyc → 인터프리팅 → machine code → 실행 이 일련의 과정을 하나로 interpreter라고 통칭하기도 한다고 합니다.
      • 또 Python File로 작성되어 위 과정을 거치는 것 이외에도, Python을 Command Line으로 입력해가며 한줄씩 실행하는 REPL 방식도 지원하는데, 이 방식은 인터프리터가 소스코드를 로딩 -> 소스코드를 토큰화 -> 토큰을 구문 트리로 변환 -> (선택적) 바이트 코드 생성 -> 프로그램 실행의 과정을 거칩니다.
      • 한줄 한줄 이 과정을 거쳐야 하기 때문에 속도도 느리고 최적화가 어렵습니다.
  • 이러한 Python Interpreter의 구현체는 여러 종류가 있는데요, 컴파일러가 C나 .Net 프레임워크, Python으로 만들어지거나 컴파일 결과가 JVM의 바이트 코드로 변환되는 등의 같은 Python 문법이여도 런타임 환경이 다양하게 존재할 수 있습니다. 보통 정통 Python인 CPython을 사용하지만, Pypy가 성능상 이점이 있어 최근에는 Pypy도 많이 사용하는 추세인 듯합니다.
  • 다만 CPython이 아닌 다른 구현체를 사용한다면, C라이브러리를 사용하지 못하는 제약이 있을 수 있으니 잘 확인해야 합니다.

https://www.devopsschool.com/blog/python-virtual-machine/

PVM의 메모리 구조

  • JVM에서는 메모리 영역이 stack, heap, static, native 의 4가지 영역으로 나뉘어 메모리 관리를 수행했는데요, Python에서는 메모리 관리를 어떻게 할까요?
  • Python도 Java와 마찬가지로 4가지 영역으로 나누어 처리하는 것을 알 수 있었습니다.
    1. Code 영역
      • 프로그램의 코드가 기계어 형태로 저장
    2. Data 영역
      • 전역(global)과 정적(static) 변수가 존재하는 영역
    3. Heap 영역
      • 객체가 저장되는 영역. 객체의 크기를 컴파일 시간에 계산할 수 없어 동적할당 기법을 사용하며, 필요에 따라 메모리 크기가 조정됨.
    4. Stack 영역
      • 함수 호출시 생성되어 지역변수와 매개 변수를 저장하는 영역
      • Compile Time에 변수들의 크기가 계산되어 함수 호출 시 자동으로 할당되고, 함수 종료 시 자동으로 해제됨.
      • Thread마다 Stack이 할당 됨.
  • 여기서 저희가 눈여겨 봐야할 곳은 stack과 heap 영역인데요, 이를 좀더 자세히 살펴보면 다음과 같습니다.
    1. Python은 Java와 다르게 int와 같은 primitive type도 객체로 취급하기 때문에 Stack 영역에서는 객체에 대한 포인터, 즉 변수와 매개 변수만을 포함합니다.
    2. 따라서 모든 객체는 Heap 영역에 할당되는데, 파이썬 객체는 PyObject로 이루어져 있습니다.
    • 이 PyObject는 객체가 몇번 할당되었는지, 메모리는 어디부터인지, 객체의 값은 어떤 것인지에 대한 정보를 포함하고 있으며, 생성되는 순간 크기가 고정됩니다.
    • 따라서 Python의 List와 같이 가변 크기의 객체들은 Java의 ArrayList와 같이 값이 어느정도 차게 되면 더 큰 list 생성 - 복사의 과정을 거치게 됩니다.
typedef struct _object {
    _PyObject_HEAD_EXTRA  /* 디버깅때 사용됨 */
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

Python의 메모리 할당

  • 위에서 언급했듯 Python에서 생성자를 통해 PyObject가 생성되며 메모리가 할당됩니다. 이렇게 개발자가 직접 메모리를 할당해줄 필요없이 Python에서 객체의 메모리 할당을 자동으로 해주는데요, Python의 메모리 할당 해주는 구성 요소를 python memory manager라고 합니다.
  • Python Memory Manager은 객체의 크기를 512Bytes를 기준으로 다른 방식으로 객체를 생성합니다.
    • 512 Bytes 이상의 객체는 힙에 할당합니다.(C-Allocator)
    • 512 Bytes 이하의 객체는 Arena, Pool, Block이라는 3계층 시스템에 의해 관리됩니다.(small object allocator)
      • Block은 실제 Python 객체가 저장되는 공간으로, Python 객체의 크기에 따라 8bytes 단위로 커집니다.
      • Pool은 Block의 묶음으로, 동일한 크기의 블록으로 구성됩니다.
      • Arena는 Pool의 묶음으로, 32bit 시스템은 256KiB, 64bit 시스템은 1MiB로 고정됩니다.
  • 이렇게 이루어진 구조에 따라 small object allocator는 아래의 과정을 거쳐 메모리를 할당합니다.
    1. Python은 새로운 작은 객체를 생성할 때, 적절한 크기의 비어 있는 Block을 찾습니다.
    2. 비어 있는 Block이 없으면, 새로운 Pool을 할당합니다.
    3. 모든 Pool이 가득 찼다면, 새로운 Arena를 할당합니다.
  • 이렇게 메모리를 계층별로 관리함으로써 메모리의 단편화를 줄이고 메모리 재사용률을 높이는 등 여러 장점을 얻을 수 있다고 합니다.

Python의 GC

  • 파이썬에서도 마찬가지로 GC를 수행하는데요, GC를 수행할 때 사용하는 값이 바로 PyObject의 ob_refcnt입니다.
  • 파이썬에서는 ob_refcnt의 값이 0이 되는, 즉 더 이상 객체가 참조되지 않는다면 GC에 의해 메모리 할당 해제 되게 됩니다. 이를 Reference- Counting 방식이라고 합니다.
  • 이때 ob_refcnt는 멀티스레드 환경에서 동시에 두 스레드가 접근한다면 race condition이 발생할 수 있는데요, 파이썬에서는 이를 막기 위해 GIL(Global Interpreter Lock)을 도입합니다.
  • 파이썬은 GIL을 사용해 한번에 하나의 스레드만 접근할 수 있게 하지만, 이로 인해 성능 문제가 발생할 수 있습니다.
  • 두번째로 두 객체가 서로 참조하고 있는 순환 참조 문제도 Memory Leak의 원인이 될 수 있는데요, Python은 GC 패키지에 순환 참조 탐지 알고리즘을 포함하고 있어서 이 문제를 해결한다고 합니다.

CUDA 라이브러리를 어떻게 불러오는 걸까?

  • CUDA에서 병렬처리를 할 수 있도록 도와주는 libcudart 라이브러리는 C++ 파일인데요, Python라이브러리인 PyTorch가 어떻게 CUDA 라이브러리를 사용할 수 있는 걸까요?
  • Python에는 ctypes 라이브러리가 있어서, C++라이브러리를 불러올 수 있는 것으로 보입니다. 실제 코드가 어디에 위치해있는지는 찾지 못했지만, pytorch Github의 Issue에서 이와 관련된 내용을 확인할 수 있었습니다.
# Clean ipython session
import torch
from ctypes import cdll
cdll.LoadLibrary('libcudart.so.12')  # Succeeds
cdll.LoadLibrary('libnvJitLink.so.12')  # Fails
  • 그럼 ctypes가 뭔지 간단하게만 살펴볼까요? 파이썬 공식 문서의 ctypes 라이브러리에 따르면 정의는 아래와 같습니다.

ctypes는 파이썬용 외부 함수(foreign function) 라이브러리입니다. C 호환 데이터형을 제공하며, DLL 또는 공유 라이브러리에 있는 함수를 호출할 수 있습니다. 이 라이브러리들을 순수 파이썬으로 감싸는 데 사용할 수 있습니다.

  • ctypes를 사용하는 간단한 예제는 여기에서 확인하실 수 있습니다.

참고

반응형